NRF24L01- communication problem by more than 3 slaves with AckPlayload

Hello gents,

i have been successfully using the examples of NRF24L01 communication by the tutorial from Robin2 - Simple nRF24L01+ Tutorial
So Robin2: Many, many thanks also from my end to this very good tutorial!

In my use case 3 slaves send back a status value and an ID Nr. to the TX (Master).
The Master will actuate a servo depending from slave ID and "active" status. If more than one slave sends back "active" the master will focus on who came first.

Now comes the problem: I simply add a 4th slave to the communication and all replies turn "empty". That means that ackData[] is somehow not containing correct status and ID.
Also interessing is that the ID is transferred when i restart or reset master Node. After reset of the slaves also the ID gets lost.

Please also see the code for master, one slave and the serial monitor output.
Other slaves differ of course only from Adress and ID.
I have carefully tried to find any other issue here but unfortunately no problem compareable.
Would be great if someone can bring some light into here because I have no clue where even to start.

~Adrian

Master:

#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <Servo.h>


#define CE_PIN   4
#define CSN_PIN  5

//each slave or receiver has its own address
const byte numSlaves = 4;
const byte slaveAddress[numSlaves][5] = {
  {'R','B','G','0','1'},
  {'R','B','G','0','2'},
  {'R','B','G','0','3'},
  {'R','B','G','0','4'}
};
Servo myServo;

RF24 radio(CE_PIN, CSN_PIN); // Create a Radio

char RequestData[10] = "RBG ID 0";

int16_t ackData[2] = {0, 0}; // to hold the two values coming from the slaves
bool newReply = false;

int pos = 10; // value for initial servoposition
int M_Counter;
int M_ID;

unsigned long currentMillis;
unsigned long prevMillis;
unsigned long txIntervalMillis = 500; // send once per 0,5 second

//===============

void setup() {

    myServo.attach(16,600,2500); //Attach Servo to Pin D0, high-pulse range from 0,6ms - 2,5ms
    myServo.write(pos);
    Serial.begin(9600);
    
    radio.begin();
    radio.setDataRate( RF24_250KBPS );
    radio.setPALevel(RF24_PA_LOW);
    radio.enableAckPayload();
    radio.setPayloadSize(sizeof(ackData));
    radio.setRetries(5,5); // delay, count
                                       // 5 gives a 1500 µsec delay which is needed for a 32 byte ackPayload
}

//=============

void loop() {

    currentMillis = millis();
    if (currentMillis - prevMillis >= txIntervalMillis) {
      sendRequest();
      if(M_Counter == 1){
        ApplyData();
      }
      M_Counter = 0;
      M_ID = 0;
    }

}

//================

void sendRequest() {
    for(byte n = 0; n < numSlaves; n++){
      // open the writing pipe with the address of a slave
      radio.stopListening();
      radio.openWritingPipe(slaveAddress[n]);
      // include the slave number in the message
      RequestData[7] = n + '0';
      bool rslt;
       rslt = radio.write( &RequestData, sizeof(RequestData) );
        // Always use sizeof() as it gives the size as the number of bytes.
        // For example if RequestData was an int sizeof() would correctly return 3

      Serial.println();
      Serial.print("====================");
      Serial.print("\n");
      Serial.print("Request sent to ");
      Serial.print(RequestData);
      Serial.print("\n");
      if(rslt){
        if (radio.available() ) { //used instead of 'radio.isAckPayloadAvailble()' shall not make a difference
          radio.read(&ackData, sizeof(ackData));
          newReply = true;
        }
        else {
          Serial.println("  Acknowledge but no data ");
        }
      }
      else {
        Serial.println("  Transmission failed");
      }
      if(validateStatus(ackData[0])){ // Display the ID and status of the Slave-RBG and return true if status is "1"
        M_Counter += 1;
        M_ID = ackData[1];
      }
    }
    prevMillis = millis();
  }
//=================

bool validateStatus(int status){
  bool check = false;

  if (newReply == true) {
    Serial.print("RBG Status = ");
    Serial.print(ackData[0]);
    Serial.print("; RBG-Num: ");
    Serial.println(ackData[1]);

    if(status == 1){
      check = true;
    } 
    newReply = false;
  }
  return check;
}

void ApplyData() {
//will clean this by "switch case"  later on.

  if(M_ID == 1){
    pos = 10;
  }
  if(M_ID == 2){
    pos = 71;
  }
  if(M_ID == 3){
    pos = 128;
  }
  if(M_ID == 4){
    pos = 184; //168,75° corresonds to 270° - translation from CrownGear to BevelGear = 1.6 - correction factor: 24° = 15
  }
  Serial.print("Servoposition: ");
  Serial.print(pos);
  myServo.write(pos);
  delay(500);
}

Slave(s):

// RBG - one slave or one receiver

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

#define CE_PIN   4
#define CSN_PIN  5

const byte thisSlaveAddress[5] = {'R','B','G','0','1'}; // This address must be changed for every slave

RF24 radio(CE_PIN, CSN_PIN);

char ReqReceived[10]; // this must match RequestData in the TX (Transmitter)
int16_t ackData[2] = {0, 0}; // the two values to be sent to the master
bool newReq = false;
bool invIR;
int Tbutton = 16; // Taster set to Pin D0

//==============

void setup() {

    pinMode(Tbutton, INPUT); // Test button defined as input
    Serial.begin(9600);
    radio.begin();
    radio.setDataRate( RF24_250KBPS );
    radio.setPALevel(RF24_PA_LOW);
    radio.openReadingPipe(1, thisSlaveAddress);

    radio.enableAckPayload();
    //radio.setPayloadSize(sizeof(ackData));
    
    radio.startListening();

    radio.writeAckPayload(1, &ackData, sizeof(ackData)); // pre-load data
}

//==========

void loop() {
    getData();
    showData(); //only to display data transfer status for development
}

//============

void getData() {
    if ( radio.available() ) {
        radio.read( &ReqReceived, sizeof(ReqReceived) );
        updateReplyData();
        newReq = true;
    }
}

//================

void showData() {
    if (newReq == true) {
        Serial.print("Request received ");
        Serial.println(ReqReceived);
        Serial.print(" RBG Status sent: ");
        Serial.print(ackData[0]);
        Serial.print("; RBG Num: ");
        Serial.println(ackData[1]);
        newReq = false;
    }
}

//================

void updateReplyData() {
    invIR = !digitalRead(Tbutton); // Invert signal from optical sensor as it returns LOW as actuation

    ackData[0] = invIR;
    ackData[1] = 1; // Fix ID of RBG-01 - must be changed for every RBG
  
    radio.writeAckPayload(1, &ackData, sizeof(ackData)); // load the payload for the next time

Serial Monitor before switch off slaves:

Serial Monitor after switch off-on slaves:

I've encountered something similar before and it took me awhile to figure out what was going on. First, make sure you have a copy of the nRF24L01+ datasheet. Take a look at the data packet format and the 9-bit Packet Control Field:

The 2-bit PID (Packet ID) is incremented on every TX packet received over the SPI. As it's a 2-bit field, it repeats every 4 transmissions. So, when you send data to four slaves, all packets received by each slave carry an identical PID to the previous packet sent to that slave. That, and the fact that you're sending identical payloads to each slave (thus identical CRC) means that every packet received is completely identical to the previous one. The slave RX nRF24L01+ sees those as duplicate packets and discards them.

You can test this theory by also sending a message to a (non-existent) 5th slave and ignoring the fact that it doesn't send a response. That will stop each slave from receiving the same PID every time.

If that test works, you can reduce the chance of this problem happening with 4 slaves by changing the content of the sent packet for every transmission. Perhaps by adding a sequence ID to the packets or even an extra random byte or two. It also helps to set the CRC length to 2 bytes for more variety.

Hello Gfvalvo, your reply is a golden nugget ! :slight_smile:
Thank you for the good hint. I just read the entire datasheet and it cleary tells exactly this.
I just tried it out by adding a 5th slave to the TX and it prooved the theory.
Wird hochgeladen: grafik.png …

I think i will try to follow your idea adding a random counter or something similar to every paypload to stabilize the whole thing a bit more.

Of course setting the length of the CRC to 2 bytes may also help.
By the way, could you help me how to access the chip control register via SPI?

Best greetings

Use the APIs provided by the library (see RF24.h):

typedef enum
{
    /** (0) represents no CRC checksum is used */
    RF24_CRC_DISABLED = 0,
    /** (1) represents CRC 8 bit checksum is used */
    RF24_CRC_8,
    /** (2) represents CRC 16 bit checksum is used */
    RF24_CRC_16
} rf24_crclength_e;





    /**
     * Set the @ref CRCLength (in bits)
     *
     * CRC cannot be disabled if auto-ack is enabled
     * @param length Specify one of the values (as defined by @ref rf24_crclength_e)
     * | @p length (enum value)     | description                    |
     * |:--------------------------:|:------------------------------:|
     * | @ref RF24_CRC_DISABLED (0) | to disable using CRC checksums |
     * | @ref RF24_CRC_8 (1)        | to use 8-bit checksums         |
     * | @ref RF24_CRC_16 (2)       | to use 16-bit checksums        |
     */
    void setCRCLength(rf24_crclength_e length);

    /**
     * Get the @ref CRCLength (in bits)
     *
     * CRC checking cannot be disabled if auto-ack is enabled
     * @return One of the values defined by @ref rf24_crclength_e.
     * See table in @ref rf24_crclength_e or setCRCLength()
     */
    rf24_crclength_e getCRCLength(void);

    /**
     * Disable CRC validation
     *
     * @warning CRC cannot be disabled if auto-ack/ESB is enabled.
     */
    void disableCRC(void);

Hey gfvalvo,

i just added a counter into the payload request and reset the counter so that size of char will not overload RequestData and....i works :slight_smile:

oid sendRequest() {
    for(byte n = 0; n < numSlaves; n++){
      //add an ID to every request so that PID will not be discarded by CRC
      snprintf(RequestData, sizeof(RequestData), "ID 0 R%lu", counter);
      counter = (counter +1) %1000; // reset counter after 999
      // open the writing pipe with the address of a slave
      radio.stopListening();
      radio.openWritingPipe(slaveAddress[n]);
      // include the slave number in the message
      RequestData[3] = n + '0';
      bool rslt;
       rslt = radio.write( &RequestData, sizeof(RequestData) );
        // Always use sizeof() as it gives the size as the number of bytes.

Do you think i would still be good advised to "tune" CRC settings?

Ohter than that. I might not fully understand why a another (here a 5th) "dead request" to a not existing Rx has ended the discard of the others PID.
Is it because Nr. 5 will break the order because PID bit will "start over" with this Nr.5 and must therefore dismiss the Nr. 1 so that it can perceive next Nr. 1 as new...? ans before new Nr. 2 comes in, the existing one is also deleted and therefore it can also be unsderstood as new req.... and so on...

Appreciate your help!
Adrian

I'd say so. The only fields used to determine a packet as duplicate (and reject it) are the PID and CRC. As you've found out, when transmitting to 4 (actually any multiple of 4) receivers in a round robin fashion, each RX is sent the same PID every time. That only leaves the CRC to differentiate packets. With an 8-bit CRC, there's a 1 in 256 chance that two different payloads will produce the same CRC (and thus the packet will be rejected). A 16-bit CRC reduces that chance to 1 in 65536.

The PID is only 2 bits, so it increments from 0 to 3 then starts over. So, with 4 (or multiple of 4) receivers, each RX will see an identical PID every time. 5 is not a multiple of 4.

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