nRF24L01 master duplex communication with multiple slaves

Hello,

I hope the wizards here can stomach another nRF24L01 question. I have read numerous forum posts here and elsewhere, and the Nordic datasheet, and spent days tinkering, and now I have no hair left because I've pulled it all out.

I have an Arduino ("Master") which communicates via nRF24L01 link with two Arduino "slave" units. Each slave will gather data from local sensors. In response to a once-per-minute single character request from the master, each slave will send its data package: first slave A, then slave B. (The master Arduino itself is controlled by the "real" master, a RasPi which is connected to it by a wired serial link.)

Master code:
<

/*Dual telemetry master transmitter v.2 --> filename transmitter-12*/

#include "RF24.h"

RF24 radio(9, 10);
const byte slaveAddress[2][5] = {
  {'R', 'x', 'A', 'A', 'A'},
  {'R', 'x', 'A', 'A', 'B'}
};
struct dataStruct {
  char id;
  float t1;
  float t2;
  float t3;
} payload;

void setup() {
  Serial1.begin(9600);
  Serial1.println(F("*** Master starting v.12***"));
  radio.begin();
  delay(1000);
  radio.setPALevel(RF24_PA_HIGH);
  radio.setDataRate( RF24_250KBPS );
  radio.setRetries(10, 15); // delay, count
}

void loop() {
  if (Serial1.available()) {
    int command = Serial1.read();
    radio.stopListening();
    if (command == 'A') {
      Serial1.println(F("Req receiver A"));
      radio.openReadingPipe(1, slaveAddress[0]);
      radio.openWritingPipe(slaveAddress[0]);
      radio.write( &command, sizeof(command));
    } else if (command == 'B') {
      Serial1.println(F("Req receiver B"));
      radio.openReadingPipe(1, slaveAddress[1]);
      radio.openWritingPipe(slaveAddress[1]);
      radio.write( &command, sizeof(command));
    }
    radio.startListening();
    delay(100); //Allow time for slave to respond

    if ( !radio.available() ) {
      Serial1.print(F("No response"));
    } else {
      radio.read( &payload, sizeof(payload) );
      Serial1.print(F("Command recd: "));
      Serial1.println(command);
      Serial1.println(payload.id);
      Serial1.println(payload.t1, 2);
      Serial1.println(payload.t2, 2);
      Serial1.println(payload.t3, 1);
    }
  }
} // main loop

Slave A code:
<

/*Dual telemetery slave receiver, v.2, unit A --->filename receiver-12a*/

#include "RF24.h"

RF24 radio(9, 10);                //CE and CSN pins to Arduino pins 9 and 10
const byte slaveAddress[5] = {'R', 'x', 'A', 'A', 'A'};

struct dataStruct {
  char id = 'A';
  float t1 = 1.23;
  float t2 = 3.14;
  float t3 = 6.89;
} payload;

char command;
int SIG_LED = 3;      //Arduino pin for signal LED

void setup() {
  pinMode(SIG_LED, OUTPUT);
  digitalWrite(SIG_LED, LOW);
  radio.begin();
  radio.setPALevel(RF24_PA_LOW);    //options MIN LOW HIGH MAX
  radio.setDataRate( RF24_250KBPS );
  radio.setRetries(3, 10); // delay, count
  radio.openWritingPipe(slaveAddress);
  radio.openReadingPipe(1, slaveAddress);
  radio.startListening();
}

void loop() {
  if ( radio.available()) {
    while (radio.available()) {
      radio.read( &command, sizeof(command) );
    }
    if (command == 'A') {
      radio.stopListening();
      signal_send();              //flash LED three times as signal that the slave is sending data pkg
      radio.write( &payload, sizeof(payload) );
      radio.startListening();
    }
  }
} // Loop

void blink_led(int ms) {
  digitalWrite(SIG_LED, HIGH);
  delay(ms);
  digitalWrite(SIG_LED, LOW);
}

void signal_send() {
  blink_led(10);
  delay(100);
  blink_led(10);
  delay(100);
  blink_led(10);
}

(Slave B code is the same except for the address, which is {'R', 'x', 'A', 'A', 'B'}

The routines above work... sort of. But there are peculiarities to the operation which demonstrate beyond doubt that there is some aspect(s) of the nRF radio operation (or the library routines) of which I am completely ignorant. To illustrate by a sequence of events:

First data request "A" sent --> slave unit A flashes but no data response is rec'd
Second data request "A" sent --> slave A flashes and correct data rec'd at master
Subsequent "A" data requests --> slave A flashes and correct data rec'd
Request "B" sent --> slave B flashes but data from A received at master
Second and subsequent "B" requests --> B flashes and correct data rec'd
Request "X" sent --> no flash, but data from B received!
Second and subsequent "X" requests --> no flashes and nothing received.

In other words, it appears that data packages are cached somewhere. The returned data package is always one request "behind."

I can accommodate this cryptic behavior via the Python script, by simply sending a dummy request, ignoring the returned data, then sending the "real" request. But I don't want to do this because I don't understand what's going on, and IME routines which just happen to work despite fundamental misunderstanding of their operation are likely to blow up at inopportune times.

Any help?

(Thanks to Robin2, BTW, for the excellent tutorial which got me up and running on a single master/slave system which works like a champ.)

Thanks....!!
Steve

Respond before blinking.

Okay, very funny.
But before I send your case of Champagne, you have to explain what's going on because I haven't a clue. But you probably already knew that.

Some sort of timing issue?

So reversing the orders of those two instructions in the slave routine, such that the LED signalling occurs just after the radio.write instruction instead of just before, definitely changes the behavior. It solves part of the problem, in that the data packets received and reported by the master now correspond to what was requested.

However, it also introduces some other problem which manifests as poor transmission. A string of "A" send requests will very reliably report back A data as expected. But if a string of "B" requests (which all report as expected) is then followed by a "A" request, the result is very often neither flash nor data response from A. The whole operation seems flaky, with dropped transmissions occurring seemingly randomly whereas before the communication was very reliable (but reporting the wrong data packet when switching from one slave to the other).

This sort of inconsistent behavior is very hard to diagnose. I'd really like to know if there is a fatal flaw somewhere in the concept or coding, or if I just need to "tune" some delay somewhere....

Add a counting field (byte) to your request packets which is incremented after each transmit.

You are probably running into the duplicate packet anomaly that can show up
in specific access patterns when the packets are constant.

There is a 2 bit counter inside the NRF,
a second packet with the same sequence number and the same CRC is considered duplicate.
It will be acknowledged to the sender, but not signaled in the receiver.

srturner:
Some sort of timing issue?

Yes, and a very obvious one.

Master

    radio.startListening();
    delay(100); //Allow time for slave to respond

Slaves

  blink_led(10);
  delay(100);
  blink_led(10);
  delay(100);
  blink_led(10);

As a general advice: don't use delays

Your code would have worked if you had implemented
"expecting a response" and "blink a LED" in a non-blocking fashion.
You should change your program to full non-blocking,
the "Respond before blinking" was only a quick fix to make your blocking code "work",
but it's a very fragile arrangement.

The second problem is buried quite deep in the NRF data sheet,
I did not suffer from it, because I always had a running counter in my packets,
which (at least) lowers the probability of false duplicates massively.
In wireless communications you have to live and deal with lost packets anyway.

You could get rid of the listening state in the master by using ack payloads.
To get fresh data you would need to send two queries,
because ack data obviously has to be loaded before the request happens.

srturner:
I have an Arduino ("Master") which communicates via nRF24L01 link with two Arduino "slave" units. Each slave will gather data from local sensors. In response to a once-per-minute single character request from the master, each slave will send its data package: first slave A, then slave B.

That seems tailor made for the ackPayload example (the second example) in my Simple nRF24L01+ Tutorial. IMHO this makes two-way communication much simpler. I have also included an example where the master communicates with two slaves.

Note the recent comment at the top of the Tutorial about the RF24 library version.

...R

The "constant packet false duplicate trap" is unrelated to ackPayload, beware.

Any kind of strict polling sequence (which is very common in these setups) raises the chance
of encountering the "same CRC same PID" that leads to the false duplicate detection.

Huge thanks to you both for your pointers on where to go. A few responses:

  1. Yes, I saw the recent note at the top of the tutorial page. It is an issue that is on my pile of things to try. The problem is, with so many variables the number of setup permutations becomes overwhelming.... I also wish the RF24 library was better documented (at least relative to what I've been able to find on github).

  2. Using ack payloads was/is a high priority for another approach. I hope to try this today. The approach that I used seemed logical at the time. I had wanted to avoid the issue of always being "behind" on the data packet, but evolution of the whole system architecture has rendered that a moot point anyway.

  3. Code blocking: point well taken in the case of flashing an LED. I can simply remove that LED code; it is present only for debugging anyway. (Case of "debugging" introducing more problems than it solves?)

But in the other instance, I though that blocking subsequent execution for a time was the whole point. Here was the thought process:

radio.startListening();

delay(100) -- a request has just been sent to the slave. But the slave may be busy in a sensor read cycle and unable to respond immediately. So instruct the master to wait for a period of time for the slave to respond. Otherwise it will immediately print "No response".

if !radio.available() (etc) -- is a nice data packet now sitting in a buffer waiting to be read and processed, or not? I had assumed it would sit there and wait until retrieved.

But I think I understand what you're getting at. I will experiment with a non-blocking approach even though I don't fully understand why this one doesn't work.

  1. I was not aware of the "constant packet false duplicate trap" (nor do I understand it at present). I can easily make millis() one of the dummy variables sent by the slave, to check for uniqueness of received packets.

Thanks again for this (and all your contributions to this forum). I now have some new things to try and perhaps a chance to let some hair grow back...

Steve

By the way... the 1 sec delay in Setup is a historical inclusion which may not be necessary. I am using Arduino Nano Every boards, which I LOVE. But I have had recurring problems with them not starting properly when first powered up, and this seemed to be related to starting the radio. I've seen that delay suggested elsewhere, and it seemed to "help" (but was not an absolute solution to the problem). So another example of the tough-to-diagnose inconsistent behavior...

S.

srturner:
I though that blocking subsequent execution for a time was the whole point.

I beg to differ.

For a request/response scenario I had the best results with

send the request, note the time and the fact that you expect a response

.... proceed with the rest of the program ...

when a response comes in, process it and reset the flag for the missing response/the timeout

if a response is expected and the timeout is reached, print "No response".

srturner:
4. I was not aware of the "constant packet false duplicate trap" (nor do I understand it at present).

DupDetectRX.png
After reception of a valid packet the chip has to look for potentially duplicated packets.
An Ack could have been lost or something other that made the sender resend the packet,
while it was received and acknowledged already.
So it looks at the packet id (2 bit) and the CRC.
If both are the same as in the last packet it is discarded.

Imagine one master and four slaves that are polled round-robin in the same sequence with a constant packet.
Only the first polling cycle works,
after that the master sees success, but the slaves silently drop the packets, all of them.

Do you understand it now?

srturner:
I can easily make millis() one of the dummy variables sent by the slave, to check for uniqueness of received packets.

A single incremented byte is smaller and faster.

The master needs the field much more. Don't send constant packets.

BTW. I prefer a system which only has peers, no masters or slaves.

Make each node listen as a ground state, only switch to TX if you want to transmit
and add a source id field to distinguish the senders.
The additional pipes come in handy to implement multicast.

DupDetectRX.png

Whandall:
I beg to differ.

For a request/response scenario I had the best results with

send the request, note the time and the fact that you expect a response

I'm happy to defer to your judgement on this. Your approach does make intuitive sense, as opposed to trying to anticipate and micromanage the order of events and their timing. As you observed, however, there still needs to be a timing element present, to distinguish between a delay at the slave from a slave non-response, since the two scenarios are handled very differently. The timing element is just structured differently, as you illustrated. The slave / remote units will be solar powered, so I will also need to handle the potential situation where one or more has insufficient juice and will never respond (until the battery is recharged).

Your second point really goes to the heart of the matter: that these (nRF) devices are sophisticated and complex, with lots going on under the hood. (Or bonnet, as the case may be.) I've read through the Nordic datasheet but certainly make no claim to have understood everything I've read. I think I could implement my system without understanding the fine details ... but I'd really rather know what I'm doing. So more study is also called for here...

Thanks again ... I've got a lot of work cut out for me. At least now I have some direction to my bumbling...

S.

An update:

  1. Using the ack approach as published in the tutorial, either "out of the box" or with appropriate modifications, seems to work perfectly. Unless I uncover some unexpected problem, this is the approach I will go with.

  2. As suggested by Whandall, I changed my former code to a nonblocking paradigm. This improves the operation and eliminates the weird crypto-buffer effect. But there are still sporadic problems, almost always when switching from a string of "B" slave requests back to "A". In these cases, slave A fails to respond to the first request (but responds fine to the second and subsequent). (And why is this problem asymmetric -- almost never occurring when switching from an A string to B???) I am going to treat these effects as a learning opportunity.

I've posted the modified code (using the original approach) below. But as far as I'm concerned this problem is solved.

Thanks very much.
S.

"Master":

/*Dual telemetry master transmitter v.3 --> filename transmitter-13*/

#include "RF24.h"

RF24 radio(9, 10);
const byte slaveAddress[2][5] = {
  {'R', 'x', 'A', 'A', 'A'},
  {'R', 'x', 'A', 'A', 'B'}
};
struct dataStruct {
  char id;
  float t1;
  float t2;
  float t3;
} payload;
long unsigned started_listening;
int max_slave_delay = 500;
bool gotdata;

void setup() {
  Serial1.begin(9600);
  Serial1.println(F("*** Master starting v.13***"));
  radio.begin();
  delay(1000);
  radio.setPALevel(RF24_PA_HIGH);
  radio.setDataRate( RF24_250KBPS );
  radio.setRetries(10, 15); // delay, count
}

void loop() {
  if (Serial1.available()) {
    int command = Serial1.read();
    radio.stopListening();
    if (command == 'A') {
      radio.openReadingPipe(1, slaveAddress[0]);
      radio.openWritingPipe(slaveAddress[0]);
      radio.write( &command, sizeof(command));
    } else if (command == 'B') {
      radio.openReadingPipe(1, slaveAddress[1]);
      radio.openWritingPipe(slaveAddress[1]);
      radio.write( &command, sizeof(command));
    }

    started_listening = millis();
    gotdata = false;
    radio.startListening();
    while (!gotdata) {
      if ( radio.available() ) {
        radio.read( &payload, sizeof(payload) );
        Serial1.println(payload.id);
        Serial1.println(payload.t1, 0);
        Serial1.println(payload.t2, 2);
        Serial1.println(payload.t3, 1);
        gotdata = true;
      }
      if ((millis() - started_listening > max_slave_delay) || (millis()) < started_listening) {
        break;
      }
    }
    if (!gotdata) {
      Serial1.print(F("No data received from "));
      Serial1.println(command);
    }
  }
} // main loop

"Slave":

/*Dual telemetery slave receiver, v.3, unit A --->filename receiver-13a*/

#include "RF24.h"

RF24 radio(9, 10);                //CE and CSN pins to Arduino pins
const byte slaveAddress[5] = {'R', 'x', 'A', 'A', 'A'};

struct dataStruct {
  char id = 'A';
  float t1 = 1.0;
  float t2 = 3.14;
  float t3 = 6.89;
} payload;

char command;
int SIG_LED = 3;      //Ard pin for signal LED
unsigned long led_started;

void setup() {
  pinMode(SIG_LED, OUTPUT);
  digitalWrite(SIG_LED, LOW);
  radio.begin();
  radio.setPALevel(RF24_PA_LOW);    //options MIN LOW HIGH MAX
  radio.setDataRate( RF24_250KBPS );
  radio.setRetries(3, 10); // delay, count
  radio.openWritingPipe(slaveAddress);
  radio.openReadingPipe(1, slaveAddress);
  radio.startListening();
}

void loop() {
  if ((millis() - led_started > 100) || (led_started < millis())) {
    digitalWrite(SIG_LED, LOW);
  }
  if ( radio.available()) {
    while (radio.available()) {
      radio.read( &command, sizeof(command) );
    }
    if (command == 'A') {
      radio.stopListening();
      digitalWrite(SIG_LED, HIGH);
      led_started = millis();
      radio.write( &payload, sizeof(payload) );
      radio.startListening();
      payload.t1 += 1;
    }
  }
} // Loop

You are still using constant packets, that's not very clever.

Well, I never claimed to be clever...
But the payload t1 variable does increment.

srturner:
But the payload t1 variable does increment.

There needs to be variation in the message from the Master. If necessary add an extra item that increments but is otherwise ignored by the slave programs. Also the variation must be such that two successive messages to the same slave cannot be identical. If messages are not being sent slaveA slaveB slaveA slaveB then maybe keep two separate incrementing variables, one for slaveA and one for slaveB

...R

Robin2:
There needs to be variation in the message from the Master.

Oh. I see now.
I had been trying to figure out why the radios would care what the master data packet was, since they have no idea what it is. But of course they DO have some idea because they calculate and use a CRC! Talk about mental blocks. I will implement this ASAP and see what happens. Even though I plan to use the "ack approach," I wouldn't mind having two alternatives to choose from, besides the intellectual pursuit of understanding these things more thoroughly.
--Mr. Dense

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.