Waveform transmission with RF24 modules

I am using two UNOs running a communication program ( one Tx and other Rx) based on the library from here : http://tmrh20.github.io/RF24/classRF24.html

Basic data transfer with a structure containing a Boolean Array + unsigned int array working good.

Now there is a requirement to transmit and receive a pulse train ( 5V amplitude and frequency varying from 5 Hz to about 100 Hz with 50:50 mark space ratio)

Just wanted to know if the Rf24 link is good enough for this ?

Since the FIFO buffer can only handle 32 bytes, i can at a time pump out 32 x 8 = 256 bits. But for the transmission to be continuous, I need to keep filling it as it is emptying out.

As i understand the 32 bytes get emptied in a rather short time as compared to a 100 Hz pulse train. The period of the pulse is 10ms and thus will need about 256 x 5 = 1280ms. Which I think is rather slow compared to the rate at which the FIFO will beam out.

Any ideas how this can be handled ??

What exactly do you want to transmit? If you want to emit the same pulse train on the receiver side in real time, it's sufficient to transmit a bit whenever the signal changes.

What exactly you want to transmit ?

Pulses from a gear flow meter.

... Its sufficient to transmit a bit whenever the signal changes ....

You mean one bit at a time ??

A bit or a byte or whatever tells the receiver that the signal has changed.

Since your pulses are symmetric, it's sufficient to only signal the raising (or falling) edge. Or you convert the pulse duration into flow values, and only transmit changes in that value...

I guess I get what you are hinting at.

Can I then set up an interrupt on the Digital Pin on the transmitter to sense Rising Edge and move the transmit code inside a Interrupt Service Routine ? This way I don't have to sit and watch when the bit changes. Would the transmit be fast enough to catch on this ??

Mogaraghu: Can I then set up an interrupt on the Digital Pin on the transmitter to sense Rising Edge

If 'the Digital Pin' happens to be an external interrupt pin, very probably.

Mogaraghu: and move the transmit code inside a Interrupt Service Routine ?

You could signal from the ISR that a transmission is necessary. IRS's should be as short as possible, better shift the processing to interruptible code.

The signal could be a bool and if needed a timestamp. In loop you would check the bool instead of the digital pin, start the transmit and clear the bool after that.

I reckon you would need to send a single pulse for every nRF24 message (meaning 31 of the 32 bytes are unused - but that does not matter).

However, I wonder if the entire concept needs to be reconsidered. Rather than send individual pulses I would be inclined to send a number representing the required pulse rate (or the interval between pulses, whichever is more convenient) and let the receiving Arduino generate the output pulse train based on that data. With that arrangement the timing of the messages would not be important.

...R

In loop you would check the bool instead of the digital pin, start the transmit and clear the bool after that.

Sounds pretty good. Maybe instead of sending every pulse I can just add the pulses inside the ISR and signal when the value crosses a threshold. This threshold will then be my measurement resolution. Will try that. Thanks Whandall.

.... I would be inclined to send a number representing the required pulse rate...

Yes Robin that is a possible scenario. But in my case the pulse is from a gear flow meter (GFM) - meaning I need to find out the rate , integrate it to get quantity and stop another process when the delivered quantity matches the required quantity. So its far more easier if I get the pulses into the Rx UNO which happens to be the main controller in the process. The Tx UNO can then be a simple slave connected to the GFM, just sending the pulses when demanded by the Rx UNO.

Mogaraghu:
But in my case the pulse is from a gear flow meter (GFM) - meaning I need to find out the rate , integrate it to get quantity and stop another process when the delivered quantity matches the required quantity.

Then I would do all the calculations on the “transmitting” Arduino and just send a “stop” message when appropriate. (And, presumably, a “start” message would have been sent at another time.)

…R

Periodic messages that carry the delivered quantity (absolut and percentaged) could be useful to monitor the process.

When the transmitting Arduino knows the number of pulses, required for the stop message, it also can output progress messages at e.g. every 10%.

Did some basic calculations… the frequency of pulse to be transmitted will never exceed 60Hz or 16 ms interval. I have also seen that the RF24 roundtrip time is around 800 to 850 microseconds. That means I can easily wait for the rising edge of the pulse on Pin 2 interrupt and only when it is received transmit the pulse.

I have created a sketch to do this and would like your views on it ( its going to be two days till I get hold of the actual hardware - so would like to be ready when it arrives )

/*
  Code to be used as a RF Transmitter and paired with a similar Reciever module.
  Both use the nRF24L01 radio modules. Using TMRh20 libraray. Code uses efficient
  call response methjod where the reciever sends back dynamic ack payloads
   Both  use the nRF24L01 radio modules.
   Pin 1 : GND of UNO
   Pin 2 : 3.3V of UNO
   Pin 3 : 2 (CE)             Changed for using with LCD shield..
   Pin 4 : 3 (CSN)            Changed for using with LCD shield.
   Pin 5 : 13 (SCK)
   Pin 6 : 11 (MOSI)
   Pin 7 : 12 (MISO)
   Pin 8 : NC

   30 June 2016 : The pair for this sketch is RF24_ReceiveGFM.ino .
*/

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

/****************** User Config ***************************/
/* Hardware configuration: Set up nRF24L01 radio on SPI bus plus pins 7 & 8 ( Original) Now 2 & 3*/
RF24 radio(2, 3);
/**********************************************************/

byte addresses[][6] = {"1Node", "2Node"};             // Radio pipe addresses for the 2 nodes to communicate.1Node is Tx and 2Node is Rx
byte rfOkLed = 5, rfErrLed = 6;
unsigned long time ;
byte interruptPin   = 2;
volatile float flowTotal;
volatile bool SendData;

char gfmVal[5];                                      // Configure your transmission data into this variable or structure


//$$$$$$$$$$$$$$$$$$$$$$$$$$$$
void setup() {

  Serial.begin(115200);
  Serial.println(F("*** STARTING TRANSMITTER *** "));

  // Setup and configure radio
  radio.begin();
  radio.enableAckPayload();                   // Allow optional ack payloads
  radio.enableDynamicPayloads();              // Ack payloads are dynamic payloads
  radio.openWritingPipe(addresses[1]);        // Both radios listen on the same pipes by default, but opposite addresses
  radio.openReadingPipe(1, addresses[0]);     // Open a reading pipe on address 0, pipe 1

  pinMode(rfOkLed, OUTPUT);
  pinMode(rfErrLed, OUTPUT);
  digitalWrite(rfOkLed, LOW);
  digitalWrite(rfErrLed, LOW);

  // SETUP THE INTERRUPT FOR GFM READING
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), gfmUpdate, RISING);
}

//$$$$$$$$$$$$$$$$$$$$$$$$$$$$
// INTERRUPT SERVICE ROUTINE FOR GFM READINGS..

void gfmUpdate() {
  flowTotal += 0.6 ;
  sendData = 1;
}

//$$$$$$$$$$$$$$$$$$$$$$$$$$$$
void loop(void)
{
  if ( sendData == 1)                                         // data send flag is set. Arrange to send it.
  {
    dtostrf ( flowTotal, 5, 1, gfmVal );                      // number, width, decimal places, buffer. On Arduino sprintf() does not support float.
    radio.stopListening();                                    // Stop listening so we can send ( is this required at all since we dont set listening at all ??)

    if ( radio.write(&gfmVal, sizeof(gfmVal)) )              // SEND THE DATA TO THE OTHER RADIO
    {
      while (radio.available())                              // ACKNOWLEDGMENT WITH PAYLOAD RECEIVED
      {
        digitalWrite(rfOkLed, HIGH);
        digitalWrite(rfErrLed, LOW);
        radio.read( &gfmVal, sizeof(gfmVal));                // Read it, and process the data as required.
        Serial.println( gfmVal);                             // This is data from rx unit
      }
    }
    else
    {
      digitalWrite(rfOkLed, LOW);
      digitalWrite(rfErrLed, HIGH);
      Serial.println(F("Sending failed."));                    // If no ack response, sending failed
    }
    sendData = 0;                                              // Last valid data sent. Reset the flag.
  }
}

// END OF LOOP

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

void roundTrip_micro()
{
  unsigned long timer = micros();
  Serial.print(F("Got response "));
  Serial.print(F(" Round-trip delay: "));
  Serial.print(timer - time);
  Serial.println(F(" microseconds"));
}

You are using pin 2 as input and output

byte interruptPin   = 2;
  attachInterrupt(digitalPinToInterrupt(interruptPin), gfmUpdate, RISING);
  RF24 radio(2, 3);

You have to guard any access to multi-byte volatile data

    noInterrupts();
    dtostrf ( flowTotal, 5, 1, gfmVal );                      // number, width, decimal places, buffer.
    interrupts();

The code only sends data and expects a possible payload of the same format, so it will definitely not communicate with its identical twin.

I would not use floats, but fractional units residing in integer values.

The field gfmVal is too short, dtostrf will use 6+ chars.

char gfmVal[5];
    dtostrf ( flowTotal, 5, 1, gfmVal );                      // number, width, decimal places, buffer.
    Serial.println( gfmVal);                             // This is data from rx unit

sendData is a bool, so please treat it as one.

  //if ( sendData == 1)
  if (sendData)

I miss calls to

  void writeAckPayload(uint8_t pipe, const void* buf, uint8_t len);
   void startListening(void);

      while (radio.available())                              // ACKNOWLEDGMENT WITH PAYLOAD RECEIVEDworks      if (isAckPayloadAvailable())is more logical and selfdocumenting.

It seems you do not understand the ackPayload process.

Thanks Whandall. The Pin 2 assignment was a goof up - the usual pitfall when re-using code. The original one did not have any interrupts.

Guarding access to multi byte volatile data - done.

I am increasing gfmVal to 6. But retaining the flowTotal as a float. Its a proven code from a earlier instance.

Boolean will be respected !

As to the last two points they are as is from the example "GettingStarted_call Response.ino" and I have tried them to work fine ( though not in the current sketch)

And if I have understood : As soon as the receiver gets a payload it sends a AckPayload and when Dynamic PayLoad is enabled, there is a provision to send valid data as part of the Acknlowledgement.

To send any acknowledgement, exactely one node has to listen to the pipe with acks enabled. To send an acknowledge payload, it has to be loaded in the receiver before a packet comes in.

I miss both in your code.

I would not use a variable length conversion and transfer a fixed amount of bytes of that.

floats a slow and should not used when not needed (or very convenient). In your application I feel floats are superfluous and complicate the code.

If you do not restrict the value of your flowTotal, loo what happens

char gfmVal[6];
char after[20];

float flowTotal = 23453.55;

void setup() {
  memset(after, 'a', 10);
  Serial.begin(115200);
  dtostrf ( flowTotal, 5, 1, gfmVal );  // number, width, decimal places, buffer.
  Serial.print(F("'"));
  Serial.print(gfmVal);
  Serial.println(F("'"));
}

void loop() {
  static unsigned long lastSecond;
  unsigned long topLoop = millis();

  if (topLoop - lastSecond > 1000) {
    lastSecond = topLoop;
  }
}
'23453.6'

Ups, it's using 8 characters,

The flowTotal value will not exceed past 99.9 - there is code in main loop that resets it if it tries to.

To send an acknowledge payload, it has to be loaded in the receiver before a packet comes in.

I am following the below example code to generate my Reciever side code...

if ( role == role_pong_back ) {
    byte pipeNo, gotByte;                          // Declare variables for the pipe and the byte received
    while( radio.available(&pipeNo)){              // Read all available payloads
      radio.read( &gotByte, 1 );                   
                                                   // Since this is a call-response. Respond directly with an ack payload.
      gotByte += 1;                                // Ack payloads are much more efficient than switching to transmit mode to respond to a call
      radio.writeAckPayload(pipeNo,&gotByte, 1 );  // This can be commented out to send empty payloads.
      Serial.print(F("Loaded next response "));
      Serial.println(gotByte);  
   }

Mogaraghu: The flowTotal value will not exceed past 99.9 - there is code in main loop that resets it if it tries to.

If that code ever fails, or you change your mind about the boundary conditions, dtostrf will corrupt memory. It is better to write code that can not fail (in an obvious way), than to rely on correct input.

We can only comment the code you show, after you have the complete code (all parts, receiver and transmitter) posted, we could have a look at it again.

Edit: ISRs should be a short as possible (instruction cycles). Floating point math is really slow.

Ok Whandall... I fully agree with you. Its been good interacting and learning in the process.

Hopefully by weekend I should be able to get going with the hardware and check out the code for both Tx and Rx.

Once I check out shall come back with the actual full code. Have a great weekend !!

So its done ! Thanks to all the different suggestions given by you Whandall I did the following :

  • Simulated the Interrupt with a non blocking time-out which I can configure as required within loop() to trigger the data transmission. Worked well for 10ms intervals and above but for values less than 10ms I was getting many "Sending Failed! "messages.
  • Checked and found the offending issue was the dtostrf() function and not really the float data that was being sent.
  • I was just planning to use the dtostrf() function instead of sprintf() to generate the required display string for a LCD. But I think I will now change the variable to an unit32_t and display using a simulated decimal.
  • For the time being the Tx and Rx pair work fine with direct float transmission upto 2ms interval. And since my final waveformn max is about 60 Hz I guess there should be no problem in sending and receiving.
  • I have also attached the screen shot of the monitor. It shows the Rx unit started after the Tx unit.

The sketches are below :
The Transmitter Section :

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

/****************** User Config ***************************/
/* Hardware configuration: Set up nRF24L01 radio on SPI bus plus pins 7 & 8 */
RF24 radio(7, 8);
/**********************************************************/

byte addresses[][6] = {"1Node", "2Node"};             // Radio pipe addresses for the 2 nodes to communicate.1Node is Tx and 2Node is Rx
byte rfOkLed = 5, rfErrLed = 6;
float flowTotal;
float flowTotal_Rx;
bool sendData;
unsigned long interval ;

//$$$$$$$$$$$$$$$$$$$$$$$$$$$$
void setup() {

  Serial.begin(115200);
  Serial.println(F("*** STARTING TRANSMITTER *** "));

  // Setup and configure radio
  radio.begin();
  radio.enableAckPayload();                   // Allow optional ack payloads
  radio.enableDynamicPayloads();              // Ack payloads are dynamic payloads
  radio.openWritingPipe(addresses[1]);        // Both radios listen on the same pipes by default, but opposite addresses
  radio.openReadingPipe(1, addresses[0]);     // Open a reading pipe on address 0, pipe 1

  pinMode(rfOkLed, OUTPUT);
  pinMode(rfErrLed, OUTPUT);
  digitalWrite(rfOkLed, LOW);
  digitalWrite(rfErrLed, LOW);
}

//$$$$$$$$$$$$$$$$$$$$$$$$$$$$
void loop(void)
{
  if ( millis() - interval > 250 )
  {
    sendData = 1;
    flowTotal += 0.6;
    interval = millis();
  }

  if (sendData)                                                 // data send flag is set. Arrange to send it.
  {
    if (flowTotal > 1000) flowTotal = 0;                       // Prevent values abouve 1000

    radio.stopListening();                                      // Stop listening so we can send ( is this required at all since we dont set listening at all ??)

    if ( radio.write(&flowTotal, sizeof(flowTotal)) )           // SEND THE DATA TO THE OTHER RADIO
    {
     while(radio.available())                                   // ACKNOWLEDGMENT WITH PAYLOAD RECEIVED?
      {
        digitalWrite(rfOkLed, HIGH);
        digitalWrite(rfErrLed, LOW);
        radio.read( &flowTotal_Rx, sizeof(flowTotal_Rx));             // Read it, and process the data as required.
        Serial.println(flowTotal_Rx);                              // This is data from rx unit
      }
    }
    else
    {
      digitalWrite(rfOkLed, LOW);
      digitalWrite(rfErrLed, HIGH);
      Serial.println(F("Sending failed."));                     // If no ack response, sending failed
    }
    sendData = 0;                                               // Last valid data sent. Reset the flag.
  }
}

// END OF LOOP

Receiver Section :

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

/****************** User Config ***************************/
/* Hardware configuration: Set up nRF24L01 radio on SPI bus */
RF24 radio(2, 3);
/**********************************************************/

byte addresses[][6] = {"1Node", "2Node"};             // Radio pipe addresses for the 2 nodes to communicate.
unsigned long timeOut;
byte rfOkLed = 5, rfErrLed = 6;
float flowTotal_Rx ; 

void setup()
{
  Serial.begin(115200);
  Serial.println(F("*** STARTING RECIEVER *** "));

  // Setup and configure radio
  radio.begin();
  radio.enableAckPayload();                     // Allow optional ack payloads
  radio.enableDynamicPayloads();                // Ack payloads are dynamic payloads
  radio.openWritingPipe(addresses[0]);          // Both radios listen on the same pipes by default, but opposite addresses
  radio.openReadingPipe(1, addresses[1]);       // Open a reading pipe on address 1, pipe 1
  radio.startListening();                       // Start listening
  radio.writeAckPayload(1, &flowTotal_Rx, sizeof(flowTotal_Rx));        // Pre-load an ack-paylod into the FIFO buffer for pipe 1

  pinMode(rfOkLed, OUTPUT);
  pinMode(rfErrLed, OUTPUT);
  digitalWrite(rfOkLed, LOW);
  digitalWrite(rfErrLed, LOW);
}

//****************************************************
void loop(void)
{
  byte pipeNo = 1;                                    // Declare variables for the pipe and the byte received
  if ( radio.available(&pipeNo))                      // Read all available payloads
  {
    digitalWrite(rfOkLed, HIGH);
    digitalWrite(rfErrLed, LOW);
    radio.read( &flowTotal_Rx, sizeof(flowTotal_Rx));       //Get the data and process at this point...

    // Since this is a call-response. Respond directly with an ack payload with dynamic data.

    radio.writeAckPayload(pipeNo, &flowTotal_Rx, sizeof(flowTotal_Rx)); // Load the dynamic payload...
    timeOut = millis();
  }
  else
  {
    if ( millis() - timeOut > 2000 )
    {
      digitalWrite(rfOkLed, LOW);
      digitalWrite(rfErrLed, HIGH);
      Serial.println("Recieve Failed !");
    }
  }
}

//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

    // Since this is a call-response. Respond directly with an ack payload with dynamic data.

It is no direct response, it is the preloaded response to the next packet.