Wednesday

Maximizing Throughput on Linux Devices using the RF24 communication Stack

 Maximizing Throughput on Linux Devices using the RF24 communication Stack

How to achieve peak performance via TCP/IP connections

 With the recent changes to the core RF24 driver improving stability, I've begun more thorough investigation & testing regarding max throughput and speed on Linux devices.

 What I've found is that the typical TCP/IP stack for Linux is designed to increase throughput for high speed, bidirectional communication devices, whereas the RF24 Comm Stack is built around the RF24 and RF52 radio devices, which can only either send or receive at a given time. To work around this problem, users are encouraged to modify the TCP/IP window sizes. This allows the system to send smaller payloads, one at a time, thus increasing throughput and overall speed of communication when using the RF24Gateway layers for Linux to Linux device communication.

To configure the window sizes for RF24Gateway, just run the following commands:

sudo sysctl net.ipv4.tcp_wmem="1500 1500 1500"
sudo sysctl net.ipv4.tcp_rmem="1500 1500 1500"

 This sets the window sizes to 1500, which is right around the MAX_PAYLOAD_SIZE configured in the RF24Network layer for Linux devices.

 Results can be tested by running the following commands before and after making this change:

iperf3 -c 10.1.3.134 -4 -t 60 - For no window limit

iperf3 -c 10.1.3.134 -4 -t 60 -w 1500 - With a 1500 byte TCP/IP window

 These changes are only temporary, but users can edit /etc/sysctl.conf to make them permanent.

 Note: These changes can severely impact or disable other network systems & services. Users are advised to put these commands into a script, to enable/disable enhanced RF24 throughput.

 With the RF24Gateway ncurses interrupt example, I'm achieving speeds up to 150-175Kbps or 20-25KB/S over TCP/IP. Up to around 30KB/s using UDP. In order to do this, one needs to modify the line gw.poll(2); and change it to gw.poll(1);. This reduces the delay in the handling of data, allowing maximum throughput.

 IPerf3 results over TCP/IP
 
This works out to around 100Kbps on average or 12.5KB/s over TCP/IP, which includes the RF24Mesh node renewing its address periodically, interrupting communication slightly. 

 
IPerf3 results over UDP
 
The results with UDP (shown above) vary depending on what bit-rate you set, in this case 130Kbps or about 16.25KB/s was the chosen bit-rate.
 
 

Saturday

The RF24 Core Library - Recent Bug Fixes Affecting Stability & Auto-Ack/Pipe 0

 The RF24 Core Library -Major Bug Fix

Recent Bug Fixes Affecting Auto-Ack/Pipe 0 and RF24 Core Lib w/Dynamic Payloads

 So after all this time developing and maintaining the RF24 core library, I found yet another bug affecting the Auto-Acknowledgement functionality of the radios. An issue had long ago been identified and fixed regarding pipe 0, where the assigned reading address would be overwritten when transmitting, since the radios use pipe 0 exclusively for transmission. This affects the RX_ADDR_P0 register.

Somehow, we never realized that the receiving address, when written to the radios after switching from TX to RX mode would interfere with the reception of Acknowledgement packets in TX mode, since it would overwrite the RX_ADDR_P0 register on the switch to RX. Now the radio library caches BOTH the RX & TX address for RX_ADDR_P0, and writes to it when switching between modes as required.

This enables full functionality of the radios on all pipes, since previously, auto-ack would not work properly on pipe 0 in some cases. The changes have been committed to the source code and will be included in the next release, after v1.4.11.

There are minor impacts to throughput, but after careful consideration, these changes were included to fully enable the radios capabilities. We are working on a more efficient resolution.

This bug was mainly discovered due to my work on the nrf_to_nrf driver for nRF52x radios, which I already had caching both the TX & RX addresses. I realized the RF24 driver didn't do that.

***

I also found a bug that appeared to affect SPI functionality on Linux devices, but it turns out it affects all devices using the RF24 core driver with Dynamic Payloads. This includes the entire RF24 Comm Stack.

I first thought it affected SPI, assumed I was getting bad data, but nothing that I tried to adjust worked. Eventually I narrowed it down to the available() function consistently returning true. Once that was identified, I then discovered it had to do with 0 length payloads, and also discovered that doing a radio.read() had no effect when this happens, the RX buffers need to be flushed. So that's what is being implemented in the RF24 Core layer. When using Dynamic Payloads, and the payload size returns either >32 or 0, the buffers need to be flushed, so the RF24 layer will now do that for both sizes.

After all this time and searching for the problem, it was one line of code that had to be changed.

I'd included failure handling in all of the RF24Gateway examples due to this bug, which would intermittently cause the radios to become unresponsive, requiring them to be restarted/reconfigured. I searched high and low for a long time to find it, but at one point I thought it came down to an issue with the network layer not being able to process information fast enough.

The current approach is for the update() function in RF24Network to return a new system type, indicating there has been corruption, and the RX buffers have been flushed. The core RF24 layer will simply return 0 for Dynamic Payload Length.

This is being patched, will be available in the source code very soon, available in the next release. The Linux installer downloads from the source code.

 

RF24Gateway now displays a count of corrupted payloads

 With these changes, I've begun keeping track of how often this happens. On faster devices like RP2040 or Raspberry Pi, it seems to be more prevalent. Of course, it happens way more on a device that is doing more reception than transmitting. In the picture above, the network has detected and flushed 32 corrupt payloads over a short period of time. This was how I was able to replicate the bug, by utilizing RF24Gateway as a testing tool, and hammering it with data from another RPi and from Arduino. Over time, the bug and its workings became clearer, so I was able to narrow it down to the network.update() function in RF24Network, and then further down the stack to the getDynamicPayloadSize() function of the RF24 core library.

 I am also now logging the data on Arduino devices, via MQTT and NodeRed. I'm testing on an Arduino Nano, Due and RP2040, to see just how often this affects slower devices. As of writing this, no data is available yet, but I am running the tests long-term, so data will come in eventually, and will report back here, on this blog post.

Update: The issue affected the RP2040 doing standard communication in my "production" environment after a few days. I've filed a ticket with Nordic in hopes of identifying if this is a known issue, new issue or other.

https://devzone.nordicsemi.com/f/nordic-q-a/121237/nrf24l01-radio-r_rx_pl_wid-returns-0 

 

 

Thursday

The nrf_to_nrf Library: Recent Updates & Changes

 The nrf_to_nrf Library: Recent Updates & Changes

Power Management and Encryption

 I've made some somewhat significant changes to the nrf_to_nrf library recently. This lib allows NRF52x based devices to communicate with each other and NRF24 based devices. 

Power Management:

The NRF52x devices are low power devices, capable of utilizing very little power when all the peripherals are disabled. There was an issue brought to my attention by a user, identifying that the radios were making use of the HF Clock, Random Number Generator, CCM Encryption peripherals and that the hardware was being enabled in the constructor instead of the begin() function. 

I was able to modify the library so that the required peripherals are en/disabled only as required and on calls to powerUp() and powerDown() functions. Users need to keep this in mind, since there may be other needs for these peripherals, which may need to be re-enabled after powering down the radio.

This is fixed in the latest release v1.2.14

Authentication & Encryption:

NRF52x devices may also have a CCM mode encryption capability built in. If so, it was not being configured correctly, and the MAC/MIC verification was actually failing prior to recent updates. Changes were made to correctly copy the MAC/MIC to the receive buffer & the modes are now set prior to encryption/decryption to ensure proper MAC/MIC integrity. A verification is also performed after decryption to ensure the integrity of the MAC/MIC. 

This will be fixed in the next release and is available in the source code on GitHub.



Monday

AutoAnalogAudio Library - nRF52 Now supports Successive Approximation Analog-to-Digital Converter for Audio Input

 AutoAnalogAudio Library - nRF52 Now supports SAADC

Support for Successive Approximation Analog-to-Digital Converter for Audio Input

I've been doing a lot of work on my AAAudio library lately in regards to nRF52 devices like the XIAO BLE SENSE 52840 or the Feather Express 52840, and just added support for the onboard SAADC. This allows users to capture input audio from a regular analog microphone instead of requiring I2S or PDM. 

After having developed support for PDM, PWM and I2S, this wasn't too complicated. I've grown to understand the nRF52 interface, which is really nice and user friendly, and the documentation is great.



To use the SAADC interface start up analog audio by calling the following:

aaAudio.dinPin = 1; //Where 1 represents the AIN0, 2 represents AIN1, etc...
aaAudio.begin(3, 1); // Where 3 represents enabling of SAADC, 1 is for PWM output



All in all the SAADC seems to work very well! I am still doing initial testing & code review, but the changes have been pushed to GitHub as usual. Users can install from ZIP to test out the new code, or wait until the next release.

Saturday

AutoAnalogAudio: Decoding and playing MP3 files direct from SD card on nRF52

AutoAnalogAudio: Decoding and playing MP3 files direct from SD card on nRF52

Having fun with nRF52840 devices

I've been playing around quite a bit with my AnalogAudioAudio library since I got it working with I2S, including creating a simple Bluetooth controlled audio player.

Today, I found a simple MP3 decoding library, and decided to test it out. It actually worked! Now I have mp3 files being decoded directly from SD card on nRF52 devices like the Adafruit Feather 52840 or XIAO BLE Sense 52840.

There is lag of a few seconds prior to playback starting, but once that takes place, it seems to play the files well. The code I am using is still in prototyping stages but it does function!



I had to edit an existing MP3 decoding library a bit, but the current code I am using is at https://github.com/TMRh20/microDecoder

Any users that want to test it out can install directly from ZIP from this repository.

I would recommend testing the AutoAnalogAudio library with WAV files before venturing into MP3 playback, but to each his own...

Some example code using the above library is shown below. It has some bugs & quirks, but it does function.

#include <SD.h>
#include <AutoAnalogAudio.h>
#include "mp3.h"  // decoder
#include "pcm.h"

mp3 MP3;
pcm audio;
AutoAnalog aaAudio;

const char* audioFilename = "noceil.mp3";
uint8_t SD_CS_PIN = 5;                      // Set this to your CS pin for the SD card/module
#define USE_I2S 1

char songName[64];
float volumeControl = 0.2;
#define AUDIO_BUFFER_SIZE 6400

void setup() {

  pinMode(9, OUTPUT);
  digitalWrite(9, HIGH);
  pinMode(6, OUTPUT);  //Connected to SD pin of MAX98357A
  digitalWrite(6, HIGH);

  Serial.begin(115200);
  while (!Serial) delay(10);

  Serial.print("Init SD card...");
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("init failed!");
    return;
  }
  Serial.println("SD init ok");

  //aaAudio.I2S_PIN_LRCK = 28;  // Use different LRCK pin for Feather Express 52840
  aaAudio.maxBufferSize = AUDIO_BUFFER_SIZE;
  aaAudio.begin(0, 1, USE_I2S);

  playAudio(audioFilename);
}

void loop() {
 
  loadBuffer();

  // Control via Serial for testing
  if (Serial.available()) {
    char c = Serial.read();
    if (c == '=') {
      volumeControl += 0.1;
    } else if (c == '-') {
      volumeControl -= 0.1;
      volumeControl = max(0.0, volumeControl);
    } else if (c == 'p') {
      playAudio("waitress.mp3");
    }
    Serial.println(volumeControl);
  }

}


File myFile;

void playAudio(const char* audioFile) {

  if (myFile) {
    myFile.close();
    MP3.end();
  }
  //Open the designated file
  myFile = SD.open(audioFile);

  MP3.begin(myFile);
  MP3.getMetadata();
  aaAudio.setSampleRate(MP3.Fs, 1);
  Serial.println(MP3.Fs);
  aaAudio.dacBitsPerSample = MP3.bitsPerSample;
  Serial.println(MP3.bitsPerSample);
}

void loadBuffer() {

  if (myFile.available() > AUDIO_BUFFER_SIZE) {
    uint32_t sampleCounter = 0;
    for (uint32_t i = 0; i < 100; i++) {
      audio = MP3.decode();
      memcpy(&aaAudio.dacBuffer16[sampleCounter], audio.interleaved, 128);  // 128 bytes
      sampleCounter += 64;                                                  // 64 samples per go
    }
    for (uint32_t i = 0; i < 6400; i++) {
      int16_t sample = aaAudio.dacBuffer16[i];
      sample *= volumeControl;
      aaAudio.dacBuffer16[i] = sample;
    }
    aaAudio.feedDAC(0, 6400);
  } else {
    myFile.seek(0);
  }
}


Tuesday

AutoAnalogAudio Library: New examples for nRF52x including a BLE controlled Audio Player

 AutoAnalogAudio Library Updates for nRF52x:

 New examples for nRF52x & a BLE controlled Audio Player

With recent updates to the AutoAnalogAudio library, I've been able to put together a bunch of examples specific to the nRF52x platforms. The examples range from examples that use the onboard PDM microphone capabilities and either an I2S or Analog (PWM output) amplifier to a BLE controlled Audio Player.




The examples also demonstrate usage of the radio capabilities as well, using the radio either at a low level (nrf_to_nrf library), capable of streaming very high-quality audio or using BLE control to playback audio from SD card. 

When recording via PDM and either re-playing to an amplifier or broadcasting via radio, fairly high sample-rates can be used along with 16-bit modes, making for very decent quality wireless audio. There are some limitations when reading from SD card, as it seems the max SPI speed on these devices isn't that fast, so users need to play around with sample rates, stereo/mono modes and 8 or 16-bit samples.

Once the AutoAnalogAudio library is installed, the nRF52x examples can be found in Arduino examples under AutoAnalogAudio/Platforms/NRF52

XIAO BLE Sense 52840 used for testing


The BLE controlled audio player uses a bunch of different peripherals and pushes the capabilities of the device a bit, but it seems to work great. I've created another example using the Adafruit Bluefruit library as well, which supports faster SD reading & higher quality playback, which I will also include in the library soon. 

Here is the current code using the standard Arduino BLE library:

/* Arduino BLE control
led Audio Player for nRF52

 *
 * This is an example of me playing around with BLE control and different
 * services/characteristics to test the AutoAnalogAudio library.
 *
 * Requirements:
 * 1. nRF52 Device (Tested on nRF52840)
*  2. SD Card with WAV files: 8-bit, 16-24kHz, Mono
 * 3. I2S or Analog Amplifier + Speaker connected
 * 4. Mobile device or 'other' with nRF Connect installed
 *
 * Connect via nRF Connect App:
 * 1. Device should come up as BLE Audio Player
 * 2. You should see:
 *   a: Common Audio
*    b: Audio Input Type:
      Send a UTF-8 String to play a file: myfileDirectory/myfilename.wav
 *   c: Audio Input Control Point:
      Send an Unsigned value between 0-10 to set the volume low-high
 */


#include <SPI.h>
#include <SD.h>
#include <ArduinoBLE.h>
#include <AutoAnalogAudio.h>

AutoAnalog aaAudio;

/************** USER CONFIG ***********/
// File to play on startup
const char* audioFilename = "far8b16k.wav";  // 8-bit @ 24kHz audio is the max over SD card while BLE is running
uint8_t SD_CS_PIN = 2;                       // Set this to your CS pin for the SD card/module
#define USE_I2S 1                            // Set this to 0 for analog (PWM) audio output instead of I2S

/*********************************************************/
/* Tested with MAX98357A I2S breakout
/* BCLK connected to Arduino D1 (p0.03)
/* LRCK connected to Arduino D3 (p0.29)
/* DIN  connected to Arduino D5 (p0.05)
/* SD   connected to Arduino D6 (p1.11)
/*********************************************************/

#define FILENAME_BUFFER_LENGTH 64
char songName[FILENAME_BUFFER_LENGTH];
float volumeControl = 0.2;
#define AUDIO_BUFFER_SIZE 1600

BLEService audioService("1853");

// BLE Audio Charactaristic
BLECharacteristic audioDataCharacteristic("2b79", BLERead | BLEWrite | BLENotify, FILENAME_BUFFER_LENGTH);
BLEByteCharacteristic audioVolumeCharactaristic("2b7b", BLERead | BLEWrite);

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);

  aaAudio.begin(0, 1, USE_I2S);  //Setup aaAudio using DAC and I2S or PWM

  // BLE initialization
  if (!BLE.begin()) {
    Serial.println("Starting BLE failed!");
    while (1) {};
  }

  BLE.setLocalName("BLE Audio Player");
  BLE.setAdvertisedService(audioService);

  audioService.addCharacteristic(audioDataCharacteristic);
  audioService.addCharacteristic(audioVolumeCharactaristic);
  BLE.addService(audioService);

  BLE.advertise();
  Serial.println("BLE Peripheral is now advertising");

  Serial.print("Init SD card...");
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("init failed!");
    return;
  }
  Serial.println("SD init ok");
  pinMode(6, OUTPUT);  //Connected to SD pin of MAX98357A
  digitalWrite(6, HIGH);

  playAudio(audioFilename);
}

void loop() {

  BLEDevice central = BLE.central();

  if (central) {

    if (central.connected()) {
      if (audioDataCharacteristic.written()) {
        memset(songName, 0, sizeof(songName));
        audioDataCharacteristic.readValue((uint8_t*)songName, FILENAME_BUFFER_LENGTH);
        playAudio(songName);
        Serial.println(songName);
      }
      if (audioVolumeCharactaristic.written()) {
        uint8_t vol;
        audioVolumeCharactaristic.readValue(vol);
        volumeControl = vol / 10.0;
        Serial.print("BLE Set Volume: ");
        Serial.println(volumeControl);
      }
    }
  }

  loadBuffer();

  // Control via Serial for testing
  if (Serial.available()) {
    char c = Serial.read();
    if (c == '=') {
      volumeControl += 0.1;
    } else if (c == '-') {
      volumeControl -= 0.1;
      volumeControl = max(0.0, volumeControl);
    } else if (c == 'p') {
      playAudio("brick/brick24.wav");
    }
    Serial.println(volumeControl);
  }
}

/*********************************************************/
/* A simple function to handle playing audio files
/*********************************************************/

File myFile;

void playAudio(const char* audioFile) {

  if (myFile) {
    myFile.close();
  }
  //Open the designated file
  myFile = SD.open(audioFile);

  myFile.seek(22);
  uint16_t var;
  uint32_t var2;
  myFile.read(&var, 2);   // Get channels (Stereo or Mono)
  myFile.read(&var2, 4);  // Get Sample Rate
  aaAudio.setSampleRate(var2, var - 1);

  myFile.seek(34);
  myFile.read(&var, 2);  // Get Bits Per Sample
  aaAudio.dacBitsPerSample = var;

  myFile.seek(44);  //Skip past the WAV header
}

void loadBuffer() {

  if (myFile.available()) {

    if (aaAudio.dacBitsPerSample == 8) {
      myFile.read(aaAudio.dacBuffer, AUDIO_BUFFER_SIZE);
      for (uint32_t i = 0; i < AUDIO_BUFFER_SIZE; i++) {
        aaAudio.dacBuffer[i] *= volumeControl;
      }
      aaAudio.feedDAC(0, AUDIO_BUFFER_SIZE);
    } else {
      myFile.read(aaAudio.dacBuffer16, AUDIO_BUFFER_SIZE);
      for (uint32_t i = 0; i < AUDIO_BUFFER_SIZE / 2; i++) {
        int16_t sample = aaAudio.dacBuffer16[i];
        sample *= volumeControl;
        aaAudio.dacBuffer16[i] = (uint16_t)sample;
      }
      aaAudio.feedDAC(0, AUDIO_BUFFER_SIZE / 2);
    }

  } else {
    myFile.seek(44);
  }
}



Thursday

AutoAnalogAudio Library & I2S Output Now Working on nRF52840

 AutoAnalogAudio Library & I2S Output Now Working on nRF52840

Playing around with high quality audio on the XIAO Sense 52840

Its been a while since my last post regarding I2S audio on the nRF52840, but I finally got it working!

It came down to too small of buffer sizes, plus incorrect pins used for the MAX98357A breakout board. Now that the main problems are figured out, I have it working. There are still some issues with buffering, as there are small clicks or pops here or there throughout playback if the buffer sizes are too small.



To get it working with the current code from GitHub users need to enable I2S by calling the begin(); function a little differently:

aaAudio.begin(0, 1, 1);  //Setup aaAudio using DAC and I2S

This example enables the 'DAC' output using I2S within the AutoAnalogAudio library.

Valid sample rates have now been modified to standard sample rates: 16kHz, 24kHz, 32kHz & 44kHz. These are the only accepted sample rates.

I've decided to leave the PWM output as the default behavior since it is simpler in nature, gives good results and works OK with smaller buffer sizes. This is important when transmitting or receiving audio over radio link, since you need pretty large buffers with I2S.

Users can set the I2S pins prior to calling the begin() function for other boards that use different pins.

aaAudio.I2S_PIN_LRCK = 28;

Default Arduino Pin-Out for MAX98357A on XIAO Sense 52840:

BCLK: D1 (P0.03)

LRCK: D3 (P0.29)

DIN: D5 (P0.05)

SD Pin needs to be set HIGH for left output

GAIN: VIN

Use buffer sizes from 3200 to 6400 samples with higher sample rates / bit-rates.

Maximizing Throughput on Linux Devices using the RF24 communication Stack

 Maximizing Throughput on Linux Devices using the RF24 communication Stack How to achieve peak performance via TCP/IP connections  With the ...