I need some help with a nRF24L01 bug

Hi everyone,
For a school project me and a classmate are making some kind of gameboy. The main thing to know is that there will be one hub and two controllers. Each controller and the hub will have one nRF24L01 module. I need to program 3 games. The first one is a game where a random timer will be generated and when the timer is over the first one to click wins.

Here is where the weird issue started, let’s say the first time the hub reads every controller for a signal. The next round it does not read anything anymore. The round after it does and so on. So 1-0-1-0-1-0…

Me and my teacher have looked over the code but since he doesn’t know anything about the RF24 library he can’t really help me.
I and my classmate have been debugging for 2 weeks and I found that the problem lies with the hub. When the Hub does read the controller and I reset the hub and try again it reads it again. I have a feeling that the module has some variable stored and that it loops.

Example:

tijdTimer = 10000; //Wacht 10 seconden. Als er niets is gedrukt dan gwn terug.
        huidigeTijd = millis(); //tijd hoelang het programma al draait.
        while (millis() - huidigeTijd < tijdTimer) //doe voor het aantal seconden alles wat in de while staat.
        {
          //Serial.write("Tijd 2");
        //animatie dat timer over is
          if(radio.available())
          {
            Serial.println("Radio is avaiable");
            char in[] = {0};
            radio.read(&in, sizeof(in)); //alles dat wordt ingelezen wordt opgeslagen in de char in.

            lcd.setCursor(0,0);
            lcd.print(" The winner is: ");
            lcd.setCursor(0,2);
            lcd.print("   Player:");
            lcd.setCursor(11,2);
            lcd.print(in[0]);
            char win[] = "T"; //maak een array met karakters genaamd text. Stop hierin "T" van tone.
            if (in[0] == '1') //Winnaar con1?
            {
              radio.stopListening(); //door te stoppen met luisteren wordt het een zender.
              radio.openWritingPipe(con1);
              radio.write(&win, sizeof(win)); //verstuur de data in de text.
              radio.startListening();
            }
            else if (in[0] == '2')//Winnaar con2?
            {
              radio.stopListening(); //door te stoppen met luisteren wordt het een zender.
              radio.openWritingPipe(con2);
              radio.write(&win, sizeof(win)); //verstuur de data in de text.
              radio.startListening();
            }

            delay(4000);
            tijdTimer = 0; //Stop de timer
          }
        }

This is the line of code in the Hub that should make sure there is something available. It however does read something one time and not the other.
On the controller side there are these lines of code:

bool end = false;
      
      while (end == false) //Geen signaal binnen? blijf wachten
      {
        //Serial.println("Wacht op signaal");

        if (digitalRead(knop) == LOW && isGedrukt == LOW) //Als de knop wordt geklikt.
        {
          char uit[] = "1"; //maak een array met karakters genaamd in. Stop hierin "1".
          Serial.println("Knop gedrukt");
          radio.stopListening(); //door te stoppen met luisteren wordt het een zender
          radio.write(&uit, sizeof(uit)); //data die in 'in' staat wordt verstuurd.
          isGedrukt = HIGH;
          radio.startListening();
        }

We have tested these lines and when the button is pressed on the controller side it does go in this If statement. The problem is not on the controller side (I think :wink: ). And because resetting the Hub every other time fixes the bug I again believe the controller is not the problem.

I hope someone knows what the bug is and could help me. I have attached the code of the hub (Menu_transceiver) and the code of one of the controllers.
I know some people will hate on the:

if()
{
}

but this is how my teacher has learned me how to code and he wants us to use it like this.

If there are any questions or you need something else like schematics or something I can provide those.
The GitHub where I store the code is:
https://github.com/ItsNotDaan/DaRan-Console

Edit 1[
I was looking over the post and noticed something that I didn’t mention.
The problem lies with the Radio.available(). One time it does find something and the other time not.
]

5.Controller1.ino (4.6 KB) 4._Menu_transciever.ino (17.1 KB)

when you do char in[] = {0};this is an array of 1 byte, initialized with 0

char are denoted with single quotes 'A'and cStrings (null terminated char arrays) are using double quotes "A"

So when you do this
char uit[] = "1";
you are actually creating a cString, this is an array of 2 bytes, the first one is ‘1’ and there is a trailing null char

so when you do
radio.write(&uit, sizeof(uit));
you are actually sending out 2 bytes and possibly confusing your receivers

as you have a byte protocol, don’t declare arrays, just use char

            char inputByte = 0;
            radio.read(& inputByte, sizeof(inputByte)); //alles dat wordt ingelezen wordt opgeslagen in de char in.

or

char win = 'T'; //maak een array met karakters genaamd text. Stop hierin "T" van tone.
            if (inputByte == '1') //Winnaar con1?
            {
              radio.stopListening(); //door te stoppen met luisteren wordt het een zender.
              radio.openWritingPipe(con1);
              radio.write(&win, sizeof(win)); //verstuur de data in de text.
              radio.startListening();
            }
            else if (inputByte == '2')//Winnaar con2?
            {
              radio.stopListening(); //door te stoppen met luisteren wordt het een zender.
              radio.openWritingPipe(con2);
              radio.write(&win, sizeof(win)); //verstuur de data in de text.
              radio.startListening();
            }

Alright, thank you. I will try this on Wednesday when i’m allowed to go to school. I will keep you updated!

I have tried the following but it did not work. Me and my teacher have had a talk and he told me it probably is a fault of the library. So the solution for now is making some kind of loop so that it looks like I pressed twice or to just automatically reset the arduino after the program.
Thanks a lot.

Me and my teacher have had a talk and he told me it probably is a fault of the library

easy to blame the library…

post the codes where you fixed the char message type. (clearly if you are sending two bytes and only reading one, don’t be surprised to find another one pending next time you read, and ‘0’ is the end of a cString)

Hi i’m sorry. The mail that you replied came into my spam inbox.
I added the chars after you recommended me to do so.
I have added English explanation for you.
The Hub code has been shrunken down for testing purposes. This hub code however still has the same bug.
Hopefully you can find something.
7.Hub-Game-1.ino (3.8 KB) DaRan-Controller.ino (5.3 KB)

a few things to try out:

Can you ensure you have a clean power source for your radios? adding a 10µF (I’ve seen it work with 100nF) capacitor next to the power pins helps. (see here).


A poor power line can make the module believe it received something when there was actually nothing.

Can you double check the module status in the setup: just after radio.begin(); add

  Serial.print("Radio aangesloten : ");
  Serial.println(radio.isChipConnected() ? "JA" : "NEE");

Since you send messages that are only 1 byte long, can you let the module know and add to the setup()
radio.setPayloadSize(sizeof(char));

if modules are close to each other during your testing, set the power to LOW to avoid interferences (in the setup)
radio.setPALevel(RF24_PA_LOW);

within the code:
can you wrap all the radio.write() calls in an if(..) to test if the sending was fine, for example

if (radio.write(&uit, sizeof(uit))) {
  Serial.print(F("UIT:")); Serial.println(uit);
} else {
  Serial.println(F("UIT: verzendfout"));
}

can you put an initial value in your local variable (won’t be a bug really but that will avoid a compiler warning)
char in = '\0';

Also you should ensure there is no message pending before you start a game, if multiple players pressed their buttons roughly at the same time, the messages will have been sent but I assume for the time being you are only testing with one controller?

(how do you plan to handle the 2 controllers? will each have its own dedicated channel or are all the modules speaking on the same pipe?)

Alright I have tried your tips but it did not manage to fix the issue.

In the PCB that we made we did add the capacitors. I did not use them while testen. Luckily I have a cheap soldering station and 10uF capacitors and I managed to add them at home. This did not fix the issue.

I have added the If for the radio.write. It does send a message from the controller. So that can be ruled out. The radio.isChipConnected does also tell me that the transceiver is connected properly.

About the code for the compiler warning, I did not understand what you meant. I know that the code will avoid a compiler warning but I do not understand the code. Do you mind explaining how it works? is ‘\0’ a command for the compiler that it does not have to give a error?

The message pending is something I have not really thought about so I will try to find out how I can “clear” the pending signals. Am I right to think lets say controller 2 presses first his signal will arrive first at the hub if they are at roughly the same distance? I know that for a very professional game I should not assume this but for my school exams this is just fine.

I have tested with more controllers. Three to be exact. It does work, only the 1-0-1-0 bug keeps happening so I wanted to fix that first. I have made a drawing with how I plan to talk to the hub and the controllers. I use different addresses.


The controllers read from there controller number + the last number is a 1.
So controller 1: Write to hub: 10000, Read from hub: 10001
Controller 2: Write to hub: 20000, Read from hub: 20001

While testing with three controllers this worked.

OK - could you try the following code

// All radio listen on pipe 0
// All radio speak on pipe 1
// Hub has one address, and all game controlers share the same address.

// Devices are differentiated by an ID
// 0 means central Hub. use anything else for game controllers
const byte deviceID = 0;

// ****************   WIRING  ****************
// nRF24L01+    CE    CSN   MISO    MOSI    SCK
// UNO           7      8     12      11     13
// MEGA          7      8     50      51     52
// ESP32       D13    D27    D19     D23    D18       (ESP32 dev module)

#include <RF24.h> // http://tmrh20.github.io/RF24/
const byte CEPin = 7;
const byte CSNPin = 8;
RF24 radio(CEPin, CSNPin);

uint8_t mainAddress[] = {0x00, 0xCE, 0xCC, 0xCE, 0xCC};
uint8_t gameAddress[] = {0x01, 0xCE, 0xCC, 0xCE, 0xCC};

struct __attribute__ ((packed)) t_message {
  uint8_t UID;
  char description;
} message;

void sendMessage(t_message &msg)
{
  radio.stopListening();
  if (!radio.write( &msg, sizeof(t_message) )) Serial.println(F("Error sending message"));
  else Serial.println(F("Message sent"));
  radio.startListening();
}

bool getMessage(t_message &msg)
{
  if (! radio.available()) return false;
  radio.read( &msg, sizeof(t_message) );
  return true;
}

void setup() {
  Serial.begin(115200);
  if (deviceID == 0) Serial.println(F("Configuring Central Hub"));
  else Serial.println(F("Configuring game controller"));

  if (!radio.begin()) {
    Serial.println(F("Could not start radio"));
    while (true) ;
  }

  if (!radio.isChipConnected()) {
    Serial.println(F("No NRF24L01 chip found"));
    while (true) ;
  }

  radio.setPALevel(RF24_PA_LOW);            // RF24_PA_MAX
  radio.setPayloadSize(sizeof(t_message));
  if (deviceID == 0) {
    radio.openWritingPipe(gameAddress);
    radio.openReadingPipe(1, mainAddress);
  } else {
    radio.openWritingPipe(mainAddress);
    radio.openReadingPipe(1, gameAddress);
  }
  radio.startListening();
  if (deviceID == 0) {
    Serial.println(F("Central Hub Ready"));
  } else {
    Serial.print(F("game controller #"));
    Serial.print(deviceID);
    Serial.println(F(" Ready!"));
  }
}

void loop() {
  int c = Serial.read();
  if ((c != -1) && (!isspace((char) c))) {
    message.UID = deviceID;
    message.description = (char) c ;
    sendMessage(message);
  }

  if (getMessage(message)) {
    Serial.print(F("Device with ID #")); Serial.print(message.UID);
    Serial.print(F(" sent command ")); Serial.println(message.description);
  }
}

adjust CEPin and CSNPin to match how your units are wired.

const byte CEPin = 7;
const byte CSNPin = 8;

I assume you use the standard SPI pins for the other connexion.

  • On the Central Hub, upload the code as it is
  • On the game controller #1, at the start of the code change the device ID const byte deviceID = 1; and upload the code
  • On the game controller #2, at the start of the code change the device ID const byte deviceID = 2; and upload the code

Now connect the 3 Arduino to USB ports (same or different computers) and open 3 Serial Monitors at 115200 bauds (the IDE can only deal with one unless you start multiple copies), use PUTTy or CoolTerm for example

The monitor connected to the “hub” (@ 115200 bauds) should display

Configuring Central Hub
Central Hub Ready

The monitor connected to the “controller 1” (@ 115200 bauds) should display

Configuring game controller
game controller #1 Ready!

The monitor connected to the “controller 2” (@ 115200 bauds) should display

Configuring game controller
game controller #2 Ready!

Try this:

  • type and send (some Serial monitors send right away) X in the hub window. It should answer Message sent
    On the two other terminal window you should see
    Device with ID #0 sent command X

  • type and send 1 in the controller #1 window. It should answer Message sent
    and on the central Hub monitor you should see
    Device with ID #1 sent command 1
    you should not see any message on the other controller

  • type and send 2 in the controller #2 window. It should answer Message sent
    and on the central Hub monitor you should see
    Device with ID #1 sent command 2
    you should not see any message on the other controller

if this works, then it’s good news:

  • your hardware is doing fine
  • you are able to broadcast a message from the central hub to all the controllers
  • you are able to send a message from a controller to the central Hub

The rest is just about writing a correct state machine to handle your game play.

can you give it a try?

Alright, I have tested the code.
It took some time because we managed to get a day at school today.
It did work! So thank you.

I will try my best to understand the code and merge it into the console.
I do have some questions:

struct __attribute__ ((packed)) t_message {
  uint8_t UID;
  char description;
} message;

How does this “Struct” work?

And im wondering how the gameAddresses work. Does every game need a new adress? I did understand it to some point but then I completely lost it.

a struct is a CC++ way to gather variables of different types together in a new type.

so if you want to send the identity of the module that is currently speaking, who it is speaking to and what is the command you could have

struct __attribute__ ((packed)) t_message {
  uint8_t originUID;
  uint8_t destinationUID;
  char command;
} ;

and now t_message is a new type, you can do

  t_message message1;
  message1. originUID = 0;  // the hub
  message1. destinationUID = 7;
  message1. command = 'R';

for example and then send message1

all the modules will get the message but they can check if the message is for them by comparing the destinationUID with their own UID for example

makes sense?


Does every game need a new adress

the way this is done is all the gaming station have the same address. This way when the central hub speaks, they all get the information. That’s the beauty of radio communication, you broadcast information and anyone with the right address can get the payload. You do have to change the UID when you upload so that they can be differentiated (0 would always be the hub, anything else is for a player).
As you would not want to have to deal with different code for different gamer stations, my recommendation is to get a 2, 3 or 4 way dip switch


that you hook up just as you would for buttons (INPUT_PULLUP) and you’ll be able to configure 4, 8 or 16 different binaries values by combining the switches. have the setup read the switches and set the UID from this.
→ this way you can have the very same code on all the Arduinos, it’s how the dip switches are configured that determines how the unit is set up

Yes I think I understand it.
So in your example the message ‘R’ should be going to “controller 7”? And the hub is the sender. The whole struct is a bit confusing to explain but it is easy to use.
But I could use this:

 t_message message1;
  message1. originUID = 0;  // the hub
  message1. destinationUID = 7;
  message1. command = 'R';

for every time I have to write a message to a controller? Although I think I still need the t_message struct. I will do some more research in the whole struct thing :wink:

As for the addresses, the whole using a individual pipe for every controller like I did is gone?:

const byte Rcon1[6] = "10000"; //Controller 1 dat wordt gelezen.
const byte Rcon2[6] = "20000"; //Controller 2 dat wordt gelezen.
const byte con1[6] = "10001"; //Adres van controller 1 voor het verzenden.
const byte con2[6] = "20001"; //Adres van controller 2 voor het verzenden.

Every controller has lets say address 1 and the hub has address 0? The controllers find if the message is for them using the destination.UID. So in theory their could be way more than 7 controllers? (A 16 bit switch could make 65536 controllers).

really if you see the struct as a package of data you can manipulate as a variable, that’s all there is to it.

Yes, the code knows how to send a struct, whatever you define the struct to be is what will be sent (within the maximum payload size which is 32 bytes)

As for the addresses, the whole using a individual pipe for every controller like I did is gone

Yes, you don’t need to worry about all this.

using different pipes could be useful as they are sent on different frequencies and so you don’t get the collision that will possibly occur with 1 pipe but the hardware deals with retransmission and this way you can have more than 6 devices.

I have done some research on how the structures work. I think I understand them decent.

void sendMessage(t_message &msg)
{
  radio.stopListening();
  if (!radio.write( &msg, sizeof(t_message) )) Serial.println(F("Error sending message"));
  else Serial.println(F("Message sent"));
  radio.startListening();
}

bool getMessage(t_message &msg)
{
  if (! radio.available()) return false;
  radio.read( &msg, sizeof(t_message) );
  return true;
}

Could you maybe explain the msg after the t_message?
Is this some kind of variable the system makes?

Lets say:

 message.UID = deviceID;
 message.description = (char) c ;
 sendMessage(message);

is being send. Does void sendMessage(t_message &msg) put the message vars in the msg? So all the data of message gets read and put into the msg “var”.

Couldn’t really find out precisely how that works.

I would think that something like this:

message.UID = deviceID;
message.description = (char) c ;
radio.write(t_message &message, sizeof(t_message)); //Send the data in 'text'.

also should work?

there are a number of things at play.

When you define a function’s parameter in C++ you give the type of the parameter, for example here I have an x parameter of type int

void saySomething(int x) {
  x = x +1;
  Serial.println(x);
}

if you have a variable holding a number and call the function

int aVariable = 100;
saySomething(aVariable); // will print 101
Serial.println(aVariable): // will print 100

I will see 101 in the serial monitor as expected but the x=x+1 you have in the function is just something local. aVariable has NOT been modified. that’s because the parameter of the function has been “passed by value”, a copy has been given to the function and the function worked with the copy, hence the original was not modified.

Now if you add an & to the parameter definition,

void saySomething(int & x) { // <== note the &
  x = x +1;
  Serial.println(x);
}

you are telling the compiler that you want to pass the parameter by reference. that means that no copy will be made, the function will operate as if it were working on the original parameter. So if I now do the same

int aVariable = 100;
saySomething(aVariable); // will print 101
Serial.println(aVariable): // will print 101

I wil get twice 101 because the x in the function was exactly like working with aVariable and thus I modified the orignal variable.


So how does this apply here? I have this function

bool getMessage(t_message &msg)
{
  if (! radio.available()) return false;
  radio.read( &msg, sizeof(t_message) );
  return true;
}

and you notice now the & in the parameter. It means that msg is passed by reference and so if the function modifies msg, it actually modifies the actual parameters that was used when calling the function.

Where C++ is a bit confusing is that the & has multiple purpose based on where you use it. For a function declaration it means ‘pass the parameter by reference’ but when used in a function call like
radio.read( &msg, sizeof(t_message) );
the & here means ‘take the address of msg’ → it’s a pointer in memory.
the function read() was not written using references, its using an old C techniques where you give the memory pointer and the function will write something there.

so radio.read() will fill up the memory pointed by msg and as msg was a reference, you are actually filling in the memory of the variable you used when calling getMessage()

so in the main loop when I do

if (getMessage(message)) {
    Serial.print(F("Device with ID #")); Serial.print(message.UID);
    Serial.print(F(" sent command ")); Serial.println(message.description);
  }

I’m calling getMessage() with the message variable and the function returns true if a message was read. so I’m getting two answers in one go: I know if a message was received, and if that’s the case, I know it’s been set into the message variable, so that’s why I can print the content.

does this help or are you now more confused? :slight_smile:

This helped me understand it better although it is a little hard to grasp.

I will just try to rewrite my code and test out if it works. That way I learn the most. :smile:
Thanks a lot and if I have some more questions I will just reply.

yes that’s the best way to learn

good luck

1 Like

I have rewritten the software just to test.

The bug sadly keeps on happening but I have had a little bit of progress.
If I replug/reset the hub Arduino (ATMega) in the computer the hub will find the controller two times so:
1-1-0-1-0-1-0… This happens everytime I restart the Arduino. Resetting the controller does not work.

Now I find this a bit weird. Last year we needed to program in assembly language and for some reason I think this could be a weird bit/address in the microcontroller that does not reset. Could this be the case?

I too find it weird that your program didn’t give me any trouble so I have removed all the other libraries but that didn’t help. So that also isn’t a problem.

The other thing that I maybe see a problem is with the While. Maybe that is messing things up. :sweat_smile:

2.DaRan_Controller.ino (4.8 KB)
3.Hub_Game1.ino (4.3 KB)