Sending analog joystick readings with nRF24L01 using Mirf library

hello everyone, I'm new on the forum and I'm already terrified of making some indentation error, so I beg for forgiveness since the beginning. I'm also quite new in the arduino programming (or to programming in general). In particular, I struggle to understand data types.

I was trying to make two boards communicate and serial print some readings of some scrappy analog joysticks found at home. I've read on the comments section of amazon that the nRF modules I've bought only work with Mirf library, so I tried to stick to it. (But if you come up with a solution involving more conventional libraries PLEASE tell me, for me it's only important that the code works).

my code is the following:

TRANSMITTER:

#include <SPI.h> 
#include <Mirf.h>
#include <nRF24L01.h> 
#include <MirfHardwareSpiDriver.h> 


 int j1PotX;  //will become data[0]
 int j1PotY;  //will become data[1]
 int j2PotX;  //will become data[2]
 int j2PotY;  //will become data[3]
 


void setup() {
 
Serial.begin(9600); 

Mirf.cePin = 9;   //Set the CE pin to D9 
Mirf.csnPin = 10; //Set the Csn pin to D10

Mirf.spi = &MirfHardwareSpi; 
Mirf.init(); //initialization nRF24L01

//Set the receiving identifier "Sen01"
Mirf.setTADDR((byte *)"Sen01"); 

//Set the number of bytes sent and received at a time
Mirf.payload = sizeof(int)*4;

//Sending channel, can fill 0~128, send and receive must be consistent.
Mirf.channel = 3; 

Mirf.config();

//Note that one Arduino writes Sender.ino and the other writes Receiver.ino. 
//The identifier here is written to Sender.ino

Serial.println("I'm Sender...");

// Set initial default values
  j1PotX = 127; // Values from 0 to 255. When Joystick is in resting position, the value is in the middle, or 127. We actually map the pot value from 0 to 1023 to 0 to 255 because that's one BYTE value
  j1PotY = 127;
  j2PotX = 127;
  j2PotY = 127;

  
}

void loop() {
 
 byte data[4];
 
  // Read all analog inputs and map them to one Byte value
  j1PotX = map(analogRead(A1), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into a BYTE value from 0 to 255
  j1PotY = map(analogRead(A0), 0, 1023, 0, 255);
  j2PotX = map(analogRead(A2), 0, 1023, 0, 255);
  j2PotY = map(analogRead(A3), 0, 1023, 0, 255);

data[0] =  j1PotX; 
data[1] =  j1PotY;
data[2] =  j2PotX; 
data[3] =  j2PotY;

Serial.println(j1PotX);
Serial.println(j1PotY);
Serial.println(j2PotX);
Serial.println(j2PotY);

Mirf.send((byte *) &data); //send the packet

//The next step can only be performed after the while loop function transmission is completed.
while(Mirf.isSending()) {}
delay(50);

}

RECEIVER:

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

int j1PotX;  //will become data[0]
int j1PotY;  //will become data[1]
int j2PotX;  //will become data[2]
int j2PotY;  //will become data[3]

void setup() {
  Serial.begin(9600);

  //---------Initial part, can't be modified at any time--------
  Mirf.cePin = 9;     //Set CE Pin to D9
  Mirf.csnPin = 10;  //Set Csn Pin to D10
  Mirf.spi = &MirfHardwareSpi; //default for Nano

  Mirf.init(); //initialization nRF24L01

  //---------Configuration part, can be modified at any time--------

  //Set the receiving identifier "Rec01"
  Mirf.setRADDR((byte *)"Rec01");

  //Set the number of bytes sent and received at a time
  Mirf.payload = sizeof(int) * 4;

  // Sending channel, can fill 0~128, send and receive must be consistent.
  Mirf.channel = 3;
  Mirf.config();

  //Note that one Arduino writes Sender.ino and the other writes Receiver.ino.
  //To identify the programwritten in Receiver.ino.

  Serial.println("I'm Receiver...");

  // Set initial default values
  j1PotX = 127; // Values from 0 to 255. When Joystick is in resting position, the value is in the middle, or 127. We actually map the pot value from 0 to 1023 to 0 to 255 because that's one BYTE value
  j1PotY = 127;
  j2PotX = 127;
  j2PotY = 127;

}

void loop() {
  //creating the buffer to store data
  byte data[4];


  //Waiting the prepared receive data:
  if (Mirf.dataReady()) {
    Mirf.getData((byte *) &data);  //Receive data to data array.
  }

  //serial monitoring data packet received
  Serial.println(data[0]);  //j1PotX
  Serial.println(data[1]);  //j1PotY
  Serial.println(data[2]);  //j2PotX
  Serial.println(data[3]);  //j2PotY

}


Now, the setup and connections all seem to work, when asking the transmitter to serial print the analog readings, the numbers match what i do on the joysticks. Also, the modules all work fine because trying the example code provided by the manifacturer, I managed to send "123" (literally) on the serial monitor of the receiver.

The problem is that, in my code, the receiver outputs on the serial monitor a set of fixed values, nonsense, like 241-17-242-5 in constant repetition.

I don't know where to look anymore. Even copy-pasting other codes that use Mirf don't work (I cannot guarantee the validity of the sources). Trying to adapt other examples with libraries like RF24.h didn't work neither, because they sent data using structs, while I've read that Mirf library only uses byte arrays for communication. Anyway, given the simplicity of my inputs, I don't understand why my code isn't working.

Thanks a lot in advance. As I said in the beginning, being this my first post, I'm sorry for eventual mistakes.

P.S.
this is the link for the devices I'm using. https://www.amazon.it/gp/product/B07ZCJPJ8B/ref=ppx_yo_dt_b_asin_title_o00_s01?ie=UTF8&psc=1
In the comment section someone pointed out the problem. But I also checked myself, testing a very simple transmit-receive example code (from a website) that used RF24.h

This the manifacturer explicative code, if that might be useful:

//TRANSMITTER
#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

void setup() {


  Serial.begin(9600);
  Mirf.cePin = 9;   //Set the CE pin to D9 4



  Mirf.csnPin = 10; //Set the Csn pin to D10

  Mirf.spi = &MirfHardwareSpi;
  Mirf.init(); //initialization nRF24L01

  //Set the receiving identifier "Sen01"
  Mirf.setRADDR((byte *)"Sen01");

  //Set the number of bytes sent and received at a time, here send an integer, write sizeof (unsigned int), actually equal to 2 bytes
  Mirf.payload = sizeof(unsigned int);

  //Sending channel, can fill 0~128, send and receive must be consistent.
  Mirf.channel = 3;

  Mirf.config();

  //Note that one Arduino writes Sender.ino and the other writes Receiver.ino.
  //The identifier here is written to Sender.ino
  Serial.println("I'm Sender...");
}

unsigned int adata = 0;



void loop() {


  adata = 123;
  //Since nRF24L01 can only send Mirf.payload data in a byte single byte array, //So all the data that needs to be transferred must be split into bytes.
  //Define a byte array to store pending data, because Mirf.payload = sizeof(unsigned int);
  //Actually the following is equal to byte data[2];
  byte data[Mirf.payload];

  //adata is unsigned int double-byte data and must be split.
  //Split the adata high and low eight:
  data[0] = adata & 0xFF;  //low eight bits to data[0],
  data[1] = adata >> 8;   //high eight bits to data[1]。

  //Settings send data to "serv1"
  Mirf.setTADDR((byte *)"Rec01");
  Mirf.send(data);

  //The next step can only be performed after the while loop function transmission is completed.
  while (Mirf.isSending()) {}
  delay(20);
}
//RECEIVER
#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

//Define a variable adata to store the final result.
unsigned int adata = 0;

void setup() {
  Serial.begin(9600);

  //---------Initial part, can't be modified at any time--------
  Mirf.cePin = 9;     //Set CE Pin to D9
  Mirf.csnPin = 10;  //Set CE Pin to D10

  Mirf.spi = &MirfHardwareSpi;
  Mirf.init(); //initialization nRF24L01

  //---------Configuration part, can be modified it at any time--------

  //Set the receiving identifier "Rev01"
  Mirf.setRADDR((byte *)"Rec01");

  //Set the number of bytes sent and received at a time, here sent an integer.
  //Write sizeof(unsigned int), which is actually equal to 2 bytes.
  Mirf.payload = sizeof(unsigned int);

  // Sending channel, can fill 0~128, send and receive must be consistent.
  Mirf.channel = 3; Mirf.config();

  //Note that one Arduino writes Sender.ino and the other writes Receiver.ino.
  //To identify the programwritten in Receiver.ino.

  Serial.println("I'm Receiver...");

}


void loop() {
  //Define a scratchpad array with a size of Mirf.payload.
  byte data[Mirf.payload];

  if (Mirf.dataReady()) //Waiting the prepared receive data.
  {

    Mirf.getData(data);  //Receive data to data array.
    //data[1]< move left 8 bits and data[0] merge, reorganize data.
    adata = (unsigned int)((data[1] << 8) | data[0]);

    Serial.print("pin=");
    Serial.println(adata);
    //Can also output double-byte data.
    //Serial.write(data[1]);
    //Serial.write(data[0]);
  }
}

You should include all the code. For example, function resetData() is missing.
Post a link to modules you purchased and where it states that the Mirf library must be used.

Yes, I'm sorry. I updated my post so that only the relevant part of the receiver code is showing. (of course it still doesn't work even in this simplified version :smiling_face_with_tear: )

You are sending four int variables which , on say a Uno, is 8 bytes:

On the receiver you are processing only four bytes:

These must match.

Which arduino board are you using?

Further, put all the print statements within the if statement so it prints only if there is something in the buffer:

1 Like

So, also in the transmitter code, when I say

what is happening to my data? I'm imposing two-byte int values inside one-byte sized spaces?

More in general, how should I manipulate the analog readings to make them compatible with:

It looks like you've made a change here in the meantime but the payload is now 4 bytes in total not 2 as you have below. The payload is the entire 4 byte array which you send in one operation.

This is OK because you ensure, with a map(), that that the int values are between 0 and 255 and so can be represented by a byte.

I don't think the following is correct. You could probably send an int array. The send() function needs to know only the start address and length (in bytes). Important is also that it matches the receiver side.

This that you cited here is the manufacturer code, not mine. I’ve posted it in the p.s. to give an example that worked (But I don’t understand the manipulations that it does with the byte array).

I tried to change the various dimensions according to your suggestion:

TRANSMITTER:
I included a new element to be sure that the payload is the same as the data that I send

 int j1PotX;  //will become data[0]
 int j1PotY;  //will become data[1]
 int j2PotX;  //will become data[2]
 int j2PotY;  //will become data[3]

 byte example[] = {100,100,100,100};

then in the setup I say:

//Set the number of bytes sent and received at a time
Mirf.payload = sizeof(example);

(it might be dumb, I know, but does it work?)

So that the transmitter side has Mirf.payload matching with the data i generate in the loop section:

 byte data[4];

  j1PotX = map(analogRead(A1), 0, 1023, 0, 255); 
  j1PotY = map(analogRead(A0), 0, 1023, 0, 255);
  j2PotX = map(analogRead(A2), 0, 1023, 0, 255);
  j2PotY = map(analogRead(A3), 0, 1023, 0, 255);

data[0] =  j1PotX; 
data[1] =  j1PotY;
data[2] =  j2PotX; 
data[3] =  j2PotY;

Mirf.send((byte *) &data); //send the packet

RECEIVER:
for the receiver I used the same strategy, using the

byte example[] = {100,100,100,100};

as a dummy to assign the right Mirf.payload. Then in the loop I use:

void loop() {
  //creating the buffer to store data
  byte data[Mirf.payload];

  //Waiting the prepared receive data:
  if (Mirf.dataReady()) {
    Mirf.getData((byte *) &data);  //Receive data to data array.

But apparently it doesn't work. So I guess the "dummy strategy" is wrong. I don't know a precise way to make all the data types and sizes specifications coherent. As I said I know very little about them

Can you create the simplest example of sending a single byte payload from a transmitter to a receiver which you can test and which works.

If you do that, post both parts, then I will attempt to convert it for the 4 byte array representing the joy sticks.

1 Like

hi 6v6gt, sorry if I answer only now, I had to do some research ahahah. But eventually I succeeded to understand how to treat integers conversion into separated bytes. That resulted to be the key problem.
The code is now working, so I'd like to share it here, if someone was interested (as I was) to a completely DIY transmitter/receiver code for rc planes, using Mirf library. It must certainly be refined, in particular to include more inputs, like buttons or potentiometers. But the essential is there.

TRANSMITTER:

//RC TRANSMISSION OF TWO JOYSTICKS//

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

void setup() {

  Serial.begin(9600);
  
  Mirf.cePin = 9;   //Set the CE pin to D9 4
  Mirf.csnPin = 10; //Set the Csn pin to D10
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init(); //initialization nRF24L01

  //Set the receiving identifier "Sen01" (included from the original manifacturer code, but obsolete)
  //Mirf.setRADDR((byte *)"Sen01");

  //Set the number of bytes sent and received at a time, here we send 4 integers,
  // so a packet of 8 bytes, 2 bytes for each integer
  Mirf.payload = sizeof(unsigned int) * 4;

  //Sending channel, can fill 0~128, send and receive must be consistent.
  Mirf.channel = 3;

  Mirf.config();

  Serial.println("I'm Sender...");
}


//defining the variables that will hold the analog readings from the joysticks
//JOYSTICK1
unsigned int j1PotX = 0;
unsigned int j1PotY = 0;
//JOYSTICK2 
unsigned int j2PotX = 0;
unsigned int j2PotY = 0;



void loop() {

  //reading the joysticks and mapping them into values from 0 to 255 (will be useful 
  //at the receiving side, for the motors and servos)
  j1PotX = map(analogRead(A1), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into value from 0 to 255
  j1PotY = map(analogRead(A0), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into value from 0 to 255
  j2PotX = map(analogRead(A2), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into value from 0 to 255
  j2PotY = map(analogRead(A3), 0, 1023, 0, 255); // Convert the analog read value from 0 to 1023 into value from 0 to 255

//(the step above is somehow contradictory, we actually don't need anymore two bytes for each integer, because, after this mapping,
// our integers look like this: "{ [0] [0] [0] [0] [0] [0] [0] [0] | [8bits representing a 0/255 number] }", so we could save space
//and send only the righten-byte of each. I didn't try to change this part because the rc transmission code was originally intended
//for full integers, and I didn't have enough confidence to mess up things, in particular because transmission/reception data
//packets must ABSOLUTELY match. I had a working code, I tried to stick to it) (the analog joysticks reading code was taken from 
//another example, and it did this mapping, again I'll stick to it).

  //Since nRF24L01 can only send Mirf.payload data byte by byte, exchanging byte arrays,
  //all the data to be transferred must be split into bytes.
  //that's why 4 integers will occupy an array of 8 bytes, let's create them this space:
  
  byte data[Mirf.payload]; //here we will store cell by cell all the halves of each integer

  //now we have to fill the data cells. each int must be splitted into two halves, then we store each half in a cell. every two cells
  //we will find a variable. the splitting procedure is the following: take a 16bit string containing the binary representation of
  //the integer, to isolate the first 8 bits (from the right, so the lowest bits) use "& 0xFF", that means to make a logic operation
  //that compares the 16bit with a 8bit string, taking only the bits from the 16 that match in position with the 8, so the first ones 
  //on the right. This will be stored in the first cell. For the remaing half, the 8bits on the left, just use the ">>8" that means
  //shifting to the right each bit by 8 steps, the result is that the first 8 bits on the right will be eliminated, shifted into the 
  //void, while the bits on the left will occupy their original place, thus making the 16bit an 8bit, with only the bits from the left 
  //half. They will be stored into the seconf cell. This process is repeated 4 times, creating 8 cells
  
  data[0] = j1PotX & 0xFF;  //low eight bits to data[0],
  data[1] = j1PotX >> 8;   //high eight bits to data[1]。

  data[2] = j1PotY & 0xFF;  //low eight bits to data[2],
  data[3] = j1PotY >> 8;   //high eight bits to data[3]。

  data[4] = j2PotX & 0xFF;  //low eight bits to data[4],
  data[5] = j2PotX >> 8;   //high eight bits to data[5]。

  data[6] = j2PotY & 0xFF;  //low eight bits to data[6],
  data[7] = j2PotY >> 8;   //high eight bits to data[7]。

  //Settings to send data
  Mirf.setTADDR((byte *)"Rec01");
  Mirf.send(data);

  //checking the data array that we created
  Serial.println(data[0] + data[1] ); //this two cells define j1PotX
  Serial.println(data[2] + data[3] ); //this two cells define j1PotY
  Serial.println(data[4] + data[5] ); //this two cells define j2PotX
  Serial.println(data[6] + data[7] ); //this two cells define j2PotY


  //The next step can only be performed after the while loop function transmission is completed.
  while (Mirf.isSending()) {}
  delay(20);
}

RECEIVER:

//RC RECEIVER OF TWO JOYSTICKS, WITH RELATIVE
//PLANE SERVOS AND MOTOR CONTROL
#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>
#include <Servo.h>

#define led 3

Servo throttle;  // create servo object to control the ESC
Servo rudderServo;
Servo elevatorServo;
Servo aileron1Servo;
Servo aileron2Servo;

unsigned long lastReceiveTime = 0;
unsigned long currentTime = 0;

int throttleValue, rudderValue, elevatorValue, aileron1Value, aileron2Value;

// Adjusting the servos responsiveness, tune it now since we don't have any other analogic
//input. Ideally we should add a potentiometer.
int travelAdjust = 1;

//Define a variable adata to store the final result.
unsigned int j1PotX = 0;
unsigned int j1PotY = 0;
unsigned int j2PotX = 0;
unsigned int j2PotY = 0;

void setup() {
  Serial.begin(9600);

  //setup of the nRF24
  Mirf.cePin = 9;     //Set CE Pin to D9
  Mirf.csnPin = 10;  //Set CE Pin to D10
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init(); //initialization nRF24L01

  //Set the receiving identifier "Rev01"
  Mirf.setRADDR((byte *)"Rec01");

  //Set the number of bytes sent and received at a time. we expect data packets of 8bytes,
  //that represent in total four integers (each uses 16bits).
  Mirf.payload = sizeof(unsigned int) * 4 ;

  // Sending channel, can fill 0~128, send and receive must be consistent.
  Mirf.channel = 3;
  Mirf.config();

  Serial.println("I'm Receiver...");

  //attaching the servo objects to their pins:
  throttle.attach(8);
  rudderServo.attach(4);
  elevatorServo.attach(5);
  aileron1Servo.attach(6);
  aileron2Servo.attach(7);
}


void loop() {

  // Check whether we keep receving data, or we have a connection between the two modules
  currentTime = millis();

  // If current time is more then 5 second since we have recived the last data, that means
  //we have lost connection If connection is lost, reset the data. It prevents
  //unwanted behavior, for example if a drone has a throttle up, if we lose connection
  //it can keep flying away if we dont reset the function
  if ( currentTime - lastReceiveTime > 5000 ) {
    resetData();
  }


  //Define a scratchpad array with a size of Mirf.payload.
  byte data[Mirf.payload];

  if (Mirf.dataReady()) { //Waiting the prepared receive data.
  
    Mirf.getData(data);  //Receive data to data array.

    //now we need to build back from each couple of cells in the array their corresponding
    //integer value, this means that we start a cast operation: "(unsigned int)" means that we
    //will convert the following elements into an unsiged int of 16bits. So we call the second
    //cell, and ask it to move to the left by 8bits, thus filling the leften part of the integer,
    //(the highest, because in binary we write numbers from right to left). then we merge using
    // "|" with the first cell of the couple, that being 8bits long, merged with a 16bit long,
    //will consequently occupy the first 8bits to the right. This will result in all the 16bits
    //filled in the order: integer = { [][][][][][][][] | [][][][][][][][] } == { data[1] | data[0] }
    //we then repeat for each integer.

    j1PotX = (unsigned int)((data[1] << 8) | data[0]);
    j1PotY = (unsigned int)((data[3] << 8) | data[2]);
    j2PotX = (unsigned int)((data[5] << 8) | data[4]);
    j2PotY = (unsigned int)((data[7] << 8) | data[6]);

    //serial monitor check
    Serial.println(j1PotX);
    Serial.println(j1PotY);
    Serial.println(j2PotX);
    Serial.println(j2PotY);

    //Can also output double-byte data.
    //Serial.write(data[1]);
    //Serial.write(data[0]);
  }
  else {
    Serial.print("No data packet");
  }


  ////////////////////////////////ACTUATION:///////////////////////////////////////////////////

  // Controlling throttle - brushless motor with ESC
  //throttleValue = constrain(j1PotY, 80, 255); // If Joysticks stays in middle, we only need values the upper values from 130 to 255
  throttleValue = map(throttleValue, 0, 255, 1000, 2000);
  throttle.writeMicroseconds(throttleValue);

  // Elevator control
  elevatorValue = map(j2PotY, 0, 255, (85 - travelAdjust), (35 + travelAdjust));
  elevatorServo.write(elevatorValue);

  // Ailerons control
  aileron1Value = map(j2PotX, 0, 255, (10 + travelAdjust), (80 - travelAdjust));
  aileron1Servo.write(aileron1Value);
  aileron2Servo.write(aileron1Value);

  // Rudder trimming function (tune the number before the "+")
  if (j1PotX > 127) {
    rudderValue = 1 + (j1PotX - 127);
  }
  if (j1PotX < 127) {
    rudderValue = 1 - (127 - j1PotX);
  }
  // Rudder control
  rudderValue = map(rudderValue, 0, 255, (10 + travelAdjust), (90 - travelAdjust));
  rudderServo.write(rudderValue);

  // Monitor the battery voltage
  int sensorValue = analogRead(A3);
  float voltage = sensorValue * (5.00 / 1023.00) * 3; // Convert the reading values from 5v to suitable 12V i
  // If voltage is below 11V turn on the LED
  if (voltage < 11) {
    digitalWrite(led, HIGH);

  }
  else {
    digitalWrite(led, LOW);
  }

}


void resetData() {
    j1PotX = 0;
    j1PotY = 0;
    j2PotX = 0;
    j2PotY = 0;
}

I tried to comment everything at a very dumb explanation level (my actual level ahah), and I'm sure it has more space for improvement and optimization.

Hope is useful

I'd never heard of the Mirf.h nrf24L01 library before. Odd that it was recommended to you because, for those devices, the tmrh20 library is usually the one of choice. Anyway, good that you sorted it out and provided an example code for others to use.

1 Like

Thanks a lot for your patience!!

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