Self Balancing Robot

I am using Arduino Nano, MPU6050, l298n motor driver module and 6v dc geared motor to create a 2 wheel self balancing robot.

I am sharing the code below:

// Uncomment to print the MPU data on the serial monitor for debugging
//#define PRINT_DEBUG_BUILD

// PID library
#include <PID_v1.h>

// These are needed for MPU
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"

#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
    #include "Wire.h"
#endif

// class default I2C address is 0x68
MPU6050 mpu;

#define INTERRUPT_PIN 2  // use pin 2 on Arduino Uno & most boards

// MPU control/status vars
bool dmpReady = false;  // set true if DMP init was successful
uint8_t mpuIntStatus;   // holds actual interrupt status byte from MPU
uint8_t devStatus;      // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount;     // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

// orientation/motion vars
Quaternion q;           // [w, x, y, z] quaternion container
VectorFloat gravity;    // [x, y, z] gravity vector
float ypr[3];           // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector
VectorInt16 gy;         // [x, y, z] gyro sensor measurements

// ================================================================
// === INTERRUPT DETECTION ROUTINE ===
// ================================================================

volatile bool mpuInterrupt = false;  // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
    mpuInterrupt = true;
}

#define PID_MIN_LIMIT -255  // Min limit for PID 
#define PID_MAX_LIMIT 255   // Max limit for PID 
#define PID_SAMPLE_TIME_IN_MILLI 10  // PID sample time in milliseconds

// The pitch angle given by MPU6050 when the robot is vertical and MPU6050 is horizontal is 0 in the ideal case.
// However, in reality, it's slightly off and we need to add some correction to keep the robot vertical.
// This is the angle correction to keep our robot standing vertically. Sometimes the robot moves in one direction so we need to adjust this.
#define SETPOINT_PITCH_ANGLE_OFFSET .1   

#define MIN_ABSOLUTE_SPEED 20  // Min motor speed 

double setpointPitchAngle = SETPOINT_PITCH_ANGLE_OFFSET;
double pitchGyroAngle = 0;
double pitchPIDOutput = 0;

double setpointYawRate = 0;
double yawGyroRate = 0;
double yawPIDOutput = 0;

#define PID_PITCH_KP 65
#define PID_PITCH_KI 500 
#define PID_PITCH_KD 1.5

#define PID_YAW_KP 0
#define PID_YAW_KI 0
#define PID_YAW_KD 0

PID pitchPID(&pitchGyroAngle, &pitchPIDOutput, &setpointPitchAngle, PID_PITCH_KP, PID_PITCH_KI, PID_PITCH_KD, DIRECT);
PID yawPID(&yawGyroRate, &yawPIDOutput, &setpointYawRate, PID_YAW_KP, PID_YAW_KI, PID_YAW_KD, DIRECT);

int enableMotor1 = 10;
int motor1Pin1 = 7;
int motor1Pin2 = 8;

int motor2Pin1 = 6;
int motor2Pin2 = 5;
int enableMotor2 = 9;

void setupPID() {
    pitchPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
    pitchPID.SetMode(AUTOMATIC);
    pitchPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);

    yawPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
    yawPID.SetMode(AUTOMATIC);
    yawPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);
}

void setupMotors() {
    pinMode(enableMotor1, OUTPUT);
    pinMode(motor1Pin1, OUTPUT);
    pinMode(motor1Pin2, OUTPUT);
  
    pinMode(enableMotor2, OUTPUT);
    pinMode(motor2Pin1, OUTPUT);
    pinMode(motor2Pin2, OUTPUT);

    rotateMotor(0, 0);
}

void setupMPU() {
    // join I2C bus (I2Cdev library doesn't do this automatically)
    #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
        Wire.begin();
        Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
    #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
        Fastwire::setup(400, true);
    #endif
  
    mpu.initialize();
    pinMode(INTERRUPT_PIN, INPUT);
    devStatus = mpu.dmpInitialize();

    // supply your own gyro offsets here, scaled for min sensitivity
    mpu.setXAccelOffset(-429); 
    mpu.setYAccelOffset(-1701); 
    mpu.setZAccelOffset(1161);   
    mpu.setXGyroOffset(-5);
    mpu.setYGyroOffset(-6);
    mpu.setZGyroOffset(15);

    // make sure it worked (returns 0 if so)
    if (devStatus == 0) {
        // Calibration Time: generate offsets and calibrate our MPU6050
        // mpu.CalibrateAccel(6);
        // mpu.CalibrateGyro(6);
        // turn on the DMP, now that it's ready
        mpu.setDMPEnabled(true);
        mpuIntStatus = mpu.getIntStatus();
        dmpReady = true;
        // get expected DMP packet size for later comparison
        packetSize = mpu.dmpGetFIFOPacketSize();
    } else {
        // ERROR!
        Serial.print(F("DMP Initialization failed (code "));
        Serial.print(devStatus);
        Serial.println(F(")"));
    }
}

void setup() {
    Serial.begin(9600);
    setupMotors();   
    setupMPU();
    setupPID();
}

void loop() {
    // if programming failed, don't try to do anything
    if (!dmpReady) return;

    // read a packet from FIFO. Get the Latest packet
    if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) {  
        mpu.dmpGetQuaternion(&q, fifoBuffer);
        mpu.dmpGetGravity(&gravity, &q);
        mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
        mpu.dmpGetGyro(&gy, fifoBuffer);

        yawGyroRate = gy.z;  // rotation rate in degrees per second
        pitchGyroAngle = ypr[1] * 180/M_PI;  // angle in degree

        pitchPID.Compute();
        yawPID.Compute();

        rotateMotor(pitchPIDOutput + yawPIDOutput, pitchPIDOutput - yawPIDOutput);

        #ifdef PRINT_DEBUG_BUILD
            Serial.print("Yaw Rate: ");
            Serial.println(yawGyroRate);
            Serial.print("Yaw PID Output: ");
            Serial.println(yawPIDOutput);
            delay(500);
        #endif
    }
}

void rotateMotor(int speed1, int speed2) {
    if (speed1 < 0) {
        digitalWrite(motor1Pin1, HIGH);
        digitalWrite(motor1Pin2, LOW);    
    } else {
        digitalWrite(motor1Pin1, LOW);
        digitalWrite(motor1Pin2, HIGH);      
    }

    if (speed2 < 0) {
        digitalWrite(motor2Pin1, HIGH);
        digitalWrite(motor2Pin2, LOW);    
    } else {
        digitalWrite(motor2Pin1, LOW);
        digitalWrite(motor2Pin2, HIGH);      
    }

    speed1 = abs(speed1) + MIN_ABSOLUTE_SPEED;
    speed2 = abs(speed2) + MIN_ABSOLUTE_SPEED;

    speed1 = constrain(speed1, MIN_ABSOLUTE_SPEED, 255);
    speed2 = constrain(speed2, MIN_ABSOLUTE_SPEED, 255);
    
    analogWrite(enableMotor1, speed1);
    analogWrite(enableMotor2, speed2);    
}

I found this code online as it is just edited the gyro offsets for mpu 6050 and tuned the PID values.

First Problem is the robot balances pretty well without the interrupt pin of mpu6050 is being connected to Arduino via digital pin 2. If I connect the interrupt pin the motors behaves abnormally. Sometimes they run at their full speed regardless of the pitch angle, or sometimes it balance for a few seconds and then motors stop completely.

But the code has defined interrupt pin. I am not sure if the interrupt is necessary and how the robot is working fine without this although it is mentioned in the code.

Second Problem is the robot occasionally tends to rotate left or right. I tried to put some values in 'Yaw PID'. But I am unable to figure out any patterns. If I put 1 to kP it becomes too much aggressive. I tried putting 0.01 it is same as putting 0. I am not sure how can I stop this abnormal rotation.

I have uploaded two short video on google drive. I am sharing the links below. I think this may provide better understanding of the situation.

1st video

2nd video

**I am using two separate battery set each consisting 2x 18650 lithium batteries. One for powering Arduino and another for powering motor driver. **

I am uploading the arduino code also.
Balanced_Robot_4.ino (6.5 KB)

1 Like

Commendable job on the balancing robot.

You should upload the code using the < CODE > button, and not use a link.

// Uncomment to print the MPU data on the serial monitor for debugging
//#define PRINT_DEBUG_BUILD

// PID library
#include <PID_v1.h>

// These are needed for MPU
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"

#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
#include "Wire.h"
#endif

// class default I2C address is 0x68
MPU6050 mpu;

#define INTERRUPT_PIN 2  // use pin 2 on Arduino Uno & most boards

// MPU control/status vars
bool dmpReady = false;  // set true if DMP init was successful
uint8_t mpuIntStatus;   // holds actual interrupt status byte from MPU
uint8_t devStatus;      // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount;     // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

// orientation/motion vars
Quaternion q;           // [w, x, y, z] quaternion container
VectorFloat gravity;    // [x, y, z] gravity vector
float ypr[3];           // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector
VectorInt16 gy;         // [x, y, z] gyro sensor measurements

// ================================================================
// === INTERRUPT DETECTION ROUTINE ===
// ================================================================

volatile bool mpuInterrupt = false;  // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
  mpuInterrupt = true;
}

#define PID_MIN_LIMIT -255  // Min limit for PID 
#define PID_MAX_LIMIT 255   // Max limit for PID 
#define PID_SAMPLE_TIME_IN_MILLI 10  // PID sample time in milliseconds

// The pitch angle given by MPU6050 when the robot is vertical and MPU6050 is horizontal is 0 in the ideal case.
// However, in reality, it's slightly off and we need to add some correction to keep the robot vertical.
// This is the angle correction to keep our robot standing vertically. Sometimes the robot moves in one direction so we need to adjust this.
#define SETPOINT_PITCH_ANGLE_OFFSET .1

#define MIN_ABSOLUTE_SPEED 20  // Min motor speed 

double setpointPitchAngle = SETPOINT_PITCH_ANGLE_OFFSET;
double pitchGyroAngle = 0;
double pitchPIDOutput = 0;

double setpointYawRate = 0;
double yawGyroRate = 0;
double yawPIDOutput = 0;

#define PID_PITCH_KP 65
#define PID_PITCH_KI 500
#define PID_PITCH_KD 1.5

#define PID_YAW_KP 0
#define PID_YAW_KI 0
#define PID_YAW_KD 0

PID pitchPID(&pitchGyroAngle, &pitchPIDOutput, &setpointPitchAngle, PID_PITCH_KP, PID_PITCH_KI, PID_PITCH_KD, DIRECT);
PID yawPID(&yawGyroRate, &yawPIDOutput, &setpointYawRate, PID_YAW_KP, PID_YAW_KI, PID_YAW_KD, DIRECT);

int enableMotor1 = 10;
int motor1Pin1 = 7;
int motor1Pin2 = 8;

int motor2Pin1 = 6;
int motor2Pin2 = 5;
int enableMotor2 = 9;

void setupPID() {
  pitchPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
  pitchPID.SetMode(AUTOMATIC);
  pitchPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);

  yawPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
  yawPID.SetMode(AUTOMATIC);
  yawPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);
}

void setupMotors() {
  pinMode(enableMotor1, OUTPUT);
  pinMode(motor1Pin1, OUTPUT);
  pinMode(motor1Pin2, OUTPUT);

  pinMode(enableMotor2, OUTPUT);
  pinMode(motor2Pin1, OUTPUT);
  pinMode(motor2Pin2, OUTPUT);

  rotateMotor(0, 0);
}

void setupMPU() {
  // join I2C bus (I2Cdev library doesn't do this automatically)
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
  Wire.begin();
  Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
  Fastwire::setup(400, true);
#endif

  mpu.initialize();
  pinMode(INTERRUPT_PIN, INPUT);
  devStatus = mpu.dmpInitialize();

  // supply your own gyro offsets here, scaled for min sensitivity
  mpu.setXAccelOffset(-429);
  mpu.setYAccelOffset(-1701);
  mpu.setZAccelOffset(1161);
  mpu.setXGyroOffset(-5);
  mpu.setYGyroOffset(-6);
  mpu.setZGyroOffset(15);

  // make sure it worked (returns 0 if so)
  if (devStatus == 0) {
    // Calibration Time: generate offsets and calibrate our MPU6050
    // mpu.CalibrateAccel(6);
    // mpu.CalibrateGyro(6);
    // turn on the DMP, now that it's ready
    mpu.setDMPEnabled(true);
    mpuIntStatus = mpu.getIntStatus();
    dmpReady = true;
    // get expected DMP packet size for later comparison
    packetSize = mpu.dmpGetFIFOPacketSize();
  } else {
    // ERROR!
    Serial.print(F("DMP Initialization failed (code "));
    Serial.print(devStatus);
    Serial.println(F(")"));
  }
}

void setup() {
  Serial.begin(9600);
  setupMotors();
  setupMPU();
  setupPID();
}

void loop() {
  // if programming failed, don't try to do anything
  if (!dmpReady) return;

  // read a packet from FIFO. Get the Latest packet
  if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) {
    mpu.dmpGetQuaternion(&q, fifoBuffer);
    mpu.dmpGetGravity(&gravity, &q);
    mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
    mpu.dmpGetGyro(&gy, fifoBuffer);

    yawGyroRate = gy.z;  // rotation rate in degrees per second
    pitchGyroAngle = ypr[1] * 180 / M_PI; // angle in degree

    pitchPID.Compute();
    yawPID.Compute();

    rotateMotor(pitchPIDOutput + yawPIDOutput, pitchPIDOutput - yawPIDOutput);

#ifdef PRINT_DEBUG_BUILD
    Serial.print("Yaw Rate: ");
    Serial.println(yawGyroRate);
    Serial.print("Yaw PID Output: ");
    Serial.println(yawPIDOutput);
    delay(500);
#endif
  }
}

void rotateMotor(int speed1, int speed2) {
  if (speed1 < 0) {
    digitalWrite(motor1Pin1, HIGH);
    digitalWrite(motor1Pin2, LOW);
  } else {
    digitalWrite(motor1Pin1, LOW);
    digitalWrite(motor1Pin2, HIGH);
  }

  if (speed2 < 0) {
    digitalWrite(motor2Pin1, HIGH);
    digitalWrite(motor2Pin2, LOW);
  } else {
    digitalWrite(motor2Pin1, LOW);
    digitalWrite(motor2Pin2, HIGH);
  }

  speed1 = abs(speed1) + MIN_ABSOLUTE_SPEED;
  speed2 = abs(speed2) + MIN_ABSOLUTE_SPEED;

  speed1 = constrain(speed1, MIN_ABSOLUTE_SPEED, 255);
  speed2 = constrain(speed2, MIN_ABSOLUTE_SPEED, 255);

  analogWrite(enableMotor1, speed1);
  analogWrite(enableMotor2, speed2);
}

Try changing "<" to "<="... it seems to me that if speed is zero, you also do not want movement.

seems not only offset und pid tuning was made.

I tried results are same as before.

Thanks for sharing your thoughts. Yes I do have plans to make it remotely controllable using nrf24l01. Please tell me how can I do this? Should I change the setpoints ? I think changing setpoint will make it move forward or backward but How to rotate it left or right ?

I doubt that. But, at some point motor A will be "<=0" and motor B will be "<0" so only one motor will be moved, then the MPU will sense a yaw approaching 90 degrees. Were the batteries low?

No, I checked with a multimeter Both battery pack has 7.2v approximately.

Thanks for sharing. I actually made a RC car using nrf24l01 module earlier. So I know the basics of how to send and receive data through it. My query is how can I move the self balancing robot.

@aaqib_saad and @Delta_G

Nice work guys, they are impressive.

I built one which I think is not too bad as a first pass, although the stability is not too good.

I wanted to do a kind of prototype as I'm new to Arduino, so it was a steep learning curve finding out how to do PWM for the motors, how to use the output from the MPU6050 as well as learning how to code in C / C++.

It uses a simple PID controller where the SP is zero tilt, the PV is the measured tilt away from vertical and the MV is the motor speed in the opposite direction. I wanted to keep it simple for my first try.

I'm not too proud of my code. I've leant a lot since then.

Self balancing robot using MKR1010 and MPU 6050

1 Like

Same here. I set up a couple of pots that let me adjust a couple of parameters on the fly. Essential for tuning.

2 Likes

Hey after a short break I'm again working on this project.
I have seen the codes of yours @Delta_G. It appears complicated to me as I am new in programming.

My queries:

  1. I have set the yaw pid values to 0. Then why the robot rotates randomly?

Another thing if I push the robot forward it moves forward in parallel direction. Same for the Backward. The robot only rotates randomly when no additional force is applied.

The right side motor tends to more liable for this abnormal rotation.
I have inversed the motor in the code but result is same.

In the starting it balances perfectly vertical. Few moments later the rotation appears as you can see on the video uploaded earlier. What is the reason for that?

Moreover, I have seen few codes for self balancing robot. They only contain One set of pid values. But my one has two sets of pid values named pitch pid and yaw pid. I am not sure what speciality it offers. I have set yaw pid to 0 but still the robot rotates.

Interestingly in the serial monitor it shows the yaw angle as I move the robot in the yaw direction. But whatever the yaw angle is the yaw pid is always showing 0, As it should be for 0 yaw pid values. Then why the robot rotates?

Another thing if I uncomment the serial debug option the robot fails to balance. I can only see the serial monitor when the motor powers are off.

  1. I think the motor noise cause the robot to randomly rotate. It randomly rotates 1 or 2 circles then slowly balance vertical for a few moment then again rotates, and it continues momentarily but it doesn't fall. I am thinking about omitting this issue for now.

Next is how to make the robot move forward and backward. I have searched on the forum but didn’t get any clear simple idea. I have read your thread but didn’t understand much.

All I know is, I need to bias the pid controller to tilt the robot towards the direction of movement.

But how do I do this?

  1. Finally, I believe (may be wrong though) my code has too much stuffs, like auto calibrate the mpu 6050. I am not using these for now.

May I request you all to go through my sketch and give me a simplified version deleting any unnecessary lines if they exists.

Please help thank you.

No, i found the code online as it is.

The purpose of the yaw controller is to control to whatever yaw / rotation angle you want. By setting the yaw loop tuning constants to zero you are effectively saying "don't control the yaw". So now any slight imbalances or irregularities in the floor may kick off a yawing movement as an offshoot of the pitch control movement, and there is nothing to prevent it.

You need to tune the yaw loop just like any other PID loop, with a combination of trial and error and rule of thumb loop tuning methods (just google it).

1 Like

Thanks you were right. I revised my code and done a bit research. After few adjustments basically in the yaw pid values the robot balance itself parallelly. I am happy with it.

Next target is to move the robot forward and backward remotely.

To do so, I added nrf24l01 module and successfully blink an led remotely while balancing the robot. I have transmitted 3 potentiometer and one switch value and received all these value successfully.

Can anyone give me any links about how I can ensure that both balancing and movement controls are correctly applied and do not conflict with each other.

I have set the angle set point from -2.5 to 2.5 based on the value received through nrf module. the robot move forward and backward but it oscillates too much. after a few moments it falls. Even when the set point is 0 it oscillates. I tried to reduce pid values but then it is unable to balance.

sharing the code below:

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

RF24 radio(3, 4);  // CE, CSN
const byte address[6] = "ABCDE";

struct packetdata {
  byte pot1;
  byte pot2;
  byte pot3;
  byte swtch1;
};
packetdata receiverdata;

// Uncomment to print the MPU data on the serial monitor for debugging
//#define PRINT_DEBUG_BUILD

// PID library
#include <PID_v1.h>

// These are needed for MPU
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"

#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
    #include "Wire.h"
#endif

MPU6050 mpu;

#define INTERRUPT_PIN 2  // use pin 2 on Arduino Uno & most boards

// MPU control/status vars
bool dmpReady = false;  // set true if DMP init was successful
uint8_t mpuIntStatus;   // holds actual interrupt status byte from MPU
uint8_t devStatus;      // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount;     // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

// orientation/motion vars
Quaternion q;           // [w, x, y, z] quaternion container
VectorFloat gravity;    // [x, y, z] gravity vector
float ypr[3];           // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector
VectorInt16 gy;         // [x, y, z] gyro sensor measurements

// ================================================================
// === INTERRUPT DETECTION ROUTINE ===
// ================================================================

volatile bool mpuInterrupt = false;  // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
    mpuInterrupt = true;
}

#define PID_MIN_LIMIT -255  // Min limit for PID 
#define PID_MAX_LIMIT 255   // Max limit for PID 
#define PID_SAMPLE_TIME_IN_MILLI 10  // PID sample time in milliseconds

#define MIN_ABSOLUTE_SPEED 15  // Min motor speed 

double setpointPitchAngle = 0;
double pitchGyroAngle = 0;
double pitchPIDOutput = 0;

double setpointYawRate = 0;
double yawGyroRate = 0;
double yawPIDOutput = 0;

#define PID_PITCH_KP 58
#define PID_PITCH_KI 400
#define PID_PITCH_KD 1.1

#define PID_YAW_KP 0.6
#define PID_YAW_KI 0.3 
#define PID_YAW_KD 0.01

PID pitchPID(&pitchGyroAngle, &pitchPIDOutput, &setpointPitchAngle, PID_PITCH_KP, PID_PITCH_KI, PID_PITCH_KD, DIRECT);
PID yawPID(&yawGyroRate, &yawPIDOutput, &setpointYawRate, PID_YAW_KP, PID_YAW_KI, PID_YAW_KD, DIRECT);

int enableMotor2 = 10;
int motor2Pin1 = 7;
int motor2Pin2 = 8;

int motor1Pin1 = 6;
int motor1Pin2 = 5;
int enableMotor1 = 9;

void setupPID() {
    pitchPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
    pitchPID.SetMode(AUTOMATIC);
    pitchPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);

    yawPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
    yawPID.SetMode(AUTOMATIC);
    yawPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);
}

void setupMotors() {
    pinMode(enableMotor1, OUTPUT);
    pinMode(motor1Pin1, OUTPUT);
    pinMode(motor1Pin2, OUTPUT);
  
    pinMode(enableMotor2, OUTPUT);
    pinMode(motor2Pin1, OUTPUT);
    pinMode(motor2Pin2, OUTPUT);

    pinMode(A0, OUTPUT);
    rotateMotor(0, 0);  // Initialize motors to stop
}

void setupMPU() {
    #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
        Wire.begin();
        Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
    #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
        Fastwire::setup(400, true);
    #endif
  
    mpu.initialize();
    pinMode(INTERRUPT_PIN, INPUT);
    devStatus = mpu.dmpInitialize();

    // Use predefined offsets instead of auto-calibration
    mpu.setXAccelOffset(-429); 
    mpu.setYAccelOffset(-1701); 
    mpu.setZAccelOffset(1161);   
    mpu.setXGyroOffset(-5);
    mpu.setYGyroOffset(-6);
    mpu.setZGyroOffset(15);

    if (devStatus == 0) {
        mpu.setDMPEnabled(true);
        mpuIntStatus = mpu.getIntStatus();
        dmpReady = true;
        packetSize = mpu.dmpGetFIFOPacketSize();
    } else {
        Serial.print(F("DMP Initialization failed (code "));
        Serial.print(devStatus);
        Serial.println(F(")"));
    }
}

void setup() {
    Serial.begin(115200);
    radio.begin();
    radio.openReadingPipe(0, address);
    radio.setPALevel(RF24_PA_MIN);
    radio.setDataRate(RF24_250KBPS);
    radio.setChannel(124);
    radio.startListening();
    setupMotors();   
    setupMPU();
    setupPID();
}

void loop() {
    static bool radioWasAvailable = false; // Track the previous state of the radio

    if (radio.available()) {
        // Radio is available
        radio.read(&receiverdata, sizeof(packetdata));
        digitalWrite(A0, receiverdata.swtch1);
        // Update setpointPitchAngle based on receiverdata.pot1
        if (receiverdata.pot1 == 127) {
            setpointPitchAngle = 0; // Center position
        } else {
            // Map pot1 (0-255) to the range -2.5 to 2.5
            setpointPitchAngle = map(receiverdata.pot1, 0, 255, -2500, 2500) / 1000.0;
        }
        
        radioWasAvailable = true; // Mark radio as available
    } else {
        // Radio is not available
        if (radioWasAvailable) {
            setpointPitchAngle = 0; // Set to 0 when radio becomes unavailable
            radioWasAvailable = false; // Mark radio as unavailable
        }
    }

    // Print the current state for debugging
    Serial.print("Radio Available: ");
    Serial.println(radioWasAvailable ? "Yes" : "No");
    Serial.print("Setpoint Pitch Angle: ");
    Serial.println(setpointPitchAngle);

    if (!dmpReady) return;

    if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) {  
        mpu.dmpGetQuaternion(&q, fifoBuffer);
        mpu.dmpGetGravity(&gravity, &q);
        mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
        mpu.dmpGetGyro(&gy, fifoBuffer);

        pitchGyroAngle = ypr[1] * 180/M_PI;  // angle in degrees
        yawGyroRate = gy.z;  // rotation rate in degrees per second

        pitchPID.Compute();
        yawPID.Compute();

        rotateMotor(pitchPIDOutput + yawPIDOutput, pitchPIDOutput - yawPIDOutput);
        
        #ifdef PRINT_DEBUG_BUILD
            Serial.print("Yaw Rate: ");
            Serial.println(yawGyroRate);
            Serial.print("Yaw PID Output: ");
            Serial.println(yawPIDOutput);
            Serial.print("Pitch Angle: ");
            Serial.println(pitchGyroAngle);
            Serial.print("PID Output: ");
            Serial.println(pitchPIDOutput);
            delay(500);   
        #endif
    }
}

void rotateMotor(int speed1, int speed2) {
    if (speed1 < 0) {
        digitalWrite(motor1Pin1, HIGH);
        digitalWrite(motor1Pin2, LOW);    
    } else {
        digitalWrite(motor1Pin1, LOW);
        digitalWrite(motor1Pin2, HIGH);      
    }

    if (speed2 < 0) {
        digitalWrite(motor2Pin1, HIGH);
        digitalWrite(motor2Pin2, LOW);    
    } else {
        digitalWrite(motor2Pin1, LOW);
        digitalWrite(motor2Pin2, HIGH);      
    }

    speed1 = abs(speed1) + MIN_ABSOLUTE_SPEED;
    speed2 = abs(speed2) + MIN_ABSOLUTE_SPEED;

    speed1 = constrain(speed1, MIN_ABSOLUTE_SPEED, 255);
    speed2 = constrain(speed2, MIN_ABSOLUTE_SPEED, 255);

    analogWrite(enableMotor1, speed1);
    analogWrite(enableMotor2, speed2);
}

Tried but doesn't work. It oscillates and gradually lose balance.

It appears that one of the data cable connections of the mpu 6050 was loose. Maybe it was the main problem. I also changed the pid values. Now forward and backward movement is good enough.

Next is to move the robot left and right. Can you please give me some hint ?

I did it. It works. Thank you everyone for your patience and support.
Sharing the code and Video below:

Receiver Side

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

RF24 radio(3, 4);  // CE, CSN
const byte address[6] = "ABCDE";

struct packetdata {
  byte pot1;
  byte pot2;
  byte pot3;
  byte swtch1;
};
packetdata receiverdata;

// Uncomment to print the MPU data on the serial monitor for debugging
//#define PRINT_DEBUG_BUILD

// PID library
#include <PID_v1.h>

// These are needed for MPU
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps20.h"

#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
    #include "Wire.h"
#endif

MPU6050 mpu;

// No need for INTERRUPT_PIN since we're not using interrupts

// MPU control/status vars
bool dmpReady = false;  // set true if DMP init was successful
uint8_t devStatus;      // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize;    // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount;     // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer

// orientation/motion vars
Quaternion q;           // [w, x, y, z] quaternion container
VectorFloat gravity;    // [x, y, z] gravity vector
float ypr[3];           // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector
VectorInt16 gy;         // [x, y, z] gyro sensor measurements

#define PID_MIN_LIMIT -255  // Min limit for PID 
#define PID_MAX_LIMIT 255   // Max limit for PID 
#define PID_SAMPLE_TIME_IN_MILLI 10  // PID sample time in milliseconds

#define MIN_ABSOLUTE_SPEED 15  // Min motor speed 

double setpointPitchAngle = 0;
double pitchGyroAngle = 0;
double pitchPIDOutput = 0;

double setpointYawRate = 0;
double yawGyroRate = 0;
double yawPIDOutput = 0;

#define PID_PITCH_KP 58
#define PID_PITCH_KI 400
#define PID_PITCH_KD 1.1

#define PID_YAW_KP 0.6
#define PID_YAW_KI 0.3 
#define PID_YAW_KD 0.01

PID pitchPID(&pitchGyroAngle, &pitchPIDOutput, &setpointPitchAngle, PID_PITCH_KP, PID_PITCH_KI, PID_PITCH_KD, DIRECT);
PID yawPID(&yawGyroRate, &yawPIDOutput, &setpointYawRate, PID_YAW_KP, PID_YAW_KI, PID_YAW_KD, DIRECT);

int enableMotor2 = 10;
int motor2Pin1 = 7;
int motor2Pin2 = 8;

int motor1Pin1 = 6;
int motor1Pin2 = 5;
int enableMotor1 = 9;

void setupPID() {
    pitchPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
    pitchPID.SetMode(AUTOMATIC);
    pitchPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);

    yawPID.SetOutputLimits(PID_MIN_LIMIT, PID_MAX_LIMIT);
    yawPID.SetMode(AUTOMATIC);
    yawPID.SetSampleTime(PID_SAMPLE_TIME_IN_MILLI);
}

void setupMotors() {
    pinMode(enableMotor1, OUTPUT);
    pinMode(motor1Pin1, OUTPUT);
    pinMode(motor1Pin2, OUTPUT);
  
    pinMode(enableMotor2, OUTPUT);
    pinMode(motor2Pin1, OUTPUT);
    pinMode(motor2Pin2, OUTPUT);

    pinMode(A0, OUTPUT);
    rotateMotor(0, 0);  // Initialize motors to stop
}

void setupMPU() {
    #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
        Wire.begin();
        Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
    #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
        Fastwire::setup(400, true);
    #endif
  
    mpu.initialize();
    devStatus = mpu.dmpInitialize();

    // Use predefined offsets instead of auto-calibration
    mpu.setXAccelOffset(-429); 
    mpu.setYAccelOffset(-1701); 
    mpu.setZAccelOffset(1161);   
    mpu.setXGyroOffset(-5);
    mpu.setYGyroOffset(-6);
    mpu.setZGyroOffset(15);

    if (devStatus == 0) {
        mpu.setDMPEnabled(true);
        dmpReady = true;
        packetSize = mpu.dmpGetFIFOPacketSize();
    } else {
        Serial.print(F("DMP Initialization failed (code "));
        Serial.print(devStatus);
        Serial.println(F(")"));
    }
}

void setup() {
    Serial.begin(115200);
    radio.begin();
    radio.openReadingPipe(0, address);
    radio.setPALevel(RF24_PA_MAX);
    radio.setDataRate(RF24_250KBPS);
    radio.setChannel(124);
    radio.startListening();
    setupMotors();   
    setupMPU();
    setupPID();
}

void loop() {
    static bool radioWasAvailable = false; // Track the previous state of the radio

    if (radio.available()) {
        // Radio is available
        radio.read(&receiverdata, sizeof(packetdata));
        digitalWrite(A0, receiverdata.swtch1);

        // Update setpointPitchAngle based on receiverdata.pot1
        if (receiverdata.pot1 == 127) {
            setpointPitchAngle = 0; // Center position
        } else {
            // Map pot1 (0-255) to the range -2.5 to 2.5 
            setpointPitchAngle = map(receiverdata.pot1, 0, 253, -2500, 2500) / 1000.0;
        }

        radioWasAvailable = true; // Mark radio as available
    } else {
        // Radio is not available
        if (radioWasAvailable) {
            setpointPitchAngle = 0; // Set to 0 when radio becomes unavailable
            radioWasAvailable = false; // Mark radio as unavailable
        }
    }

    // Print the current state for debugging
    Serial.print("Radio Available: ");
    Serial.println(radioWasAvailable ? "Yes" : "No");
    Serial.print("Setpoint Pitch Angle: ");
    Serial.println(setpointPitchAngle);

    if (!dmpReady) return;

    fifoCount = mpu.getFIFOCount();

    if (fifoCount >= packetSize) {
        // Read a packet from the FIFO
        mpu.getFIFOBytes(fifoBuffer, packetSize);
        
        // Process the data
        mpu.dmpGetQuaternion(&q, fifoBuffer);
        mpu.dmpGetGravity(&gravity, &q);
        mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
        mpu.dmpGetGyro(&gy, fifoBuffer);

        pitchGyroAngle = ypr[1] * 180 / M_PI;  // angle in degrees

        pitchPID.Compute();

        int motorAdjustment = 0;  // Default adjustment value

        if (radioWasAvailable && receiverdata.pot2 == 127) {
            // If pot2 is centered and radio data is available, use the yaw PID for motor control
            yawGyroRate = gy.z;  // rotation rate in degrees per second
            yawPID.Compute();
        } else if (radioWasAvailable) {
            // Disable yaw PID by setting yawPIDOutput to 0 when radio data is available and pot2 is not 127
            yawPIDOutput = 0;

            // Map receiverdata.pot2 (0-255) to (-75, 75) for manual adjustment
            motorAdjustment = map(receiverdata.pot2, 0, 255, -100, 100);
        } else {
            // When radio data is unavailable, keep yaw PID active
            yawGyroRate = gy.z;
            yawPID.Compute();
        }

        // Calculate the motor speeds
        int motorSpeed1 = pitchPIDOutput + yawPIDOutput + motorAdjustment;
        int motorSpeed2 = pitchPIDOutput - yawPIDOutput - motorAdjustment;

        // Constrain motor speeds to ensure they stay within the valid range
        motorSpeed1 = constrain(motorSpeed1, -255, 255);
        motorSpeed2 = constrain(motorSpeed2, -255, 255);

        rotateMotor(motorSpeed1, motorSpeed2);
        
        #ifdef PRINT_DEBUG_BUILD
            Serial.print("Yaw Rate: ");
            Serial.println(yawGyroRate);
            Serial.print("Yaw PID Output: ");
            Serial.println(yawPIDOutput);
            Serial.print("Pitch Angle: ");
            Serial.println(pitchGyroAngle);
            Serial.print("Pitch PID Output: ");
            Serial.println(pitchPIDOutput);
            Serial.print("Motor Adjustment: ");
            Serial.println(motorAdjustment);
            Serial.print("Motor Speed 1: ");
            Serial.println(motorSpeed1);
            Serial.print("Motor Speed 2: ");
            Serial.println(motorSpeed2);
            delay(500);   
        #endif
    }
}
void rotateMotor(int speed1, int speed2) {
    if (speed1 < 0) {
        digitalWrite(motor1Pin1, HIGH);
        digitalWrite(motor1Pin2, LOW);    
    } else {
        digitalWrite(motor1Pin1, LOW);
        digitalWrite(motor1Pin2, HIGH);      
    }

    if (speed2 < 0) {
        digitalWrite(motor2Pin1, HIGH);
        digitalWrite(motor2Pin2, LOW);    
    } else {
        digitalWrite(motor2Pin1, LOW);
        digitalWrite(motor2Pin2, HIGH);      
    }

    // Ensure that the minimum speed is respected, but constrain within valid range
    speed1 = constrain(abs(speed1) + MIN_ABSOLUTE_SPEED, MIN_ABSOLUTE_SPEED, 255);
    speed2 = constrain(abs(speed2) + MIN_ABSOLUTE_SPEED, MIN_ABSOLUTE_SPEED, 255);

    analogWrite(enableMotor1, speed1);
    analogWrite(enableMotor2, speed2);
}

Transmitter Side

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

//create an RF24 object
RF24 radio(7, 8);  // CE, CSN

//address through which two modules communicate.
const byte address[6] = "ABCDE";

struct packetdata
{
  byte pot1;
  byte pot2;
  byte pot3;
  byte swtch1;
};
packetdata data;

void setup()
{
  radio.begin();
  Serial.begin(115200);
  //set the address
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_MAX);
  radio.setDataRate(RF24_250KBPS);
  radio.setChannel(124);
  //Set module as transmitter
  radio.stopListening();
  
  pinMode(A0,INPUT);
  pinMode(A1,INPUT);
  pinMode(A3,INPUT);
  pinMode(A2,INPUT_PULLUP);     
}

int mapAndAdjustJoystickDeadBandValues(int value, bool reverse)
{
  if (value >= 540)
  {
    value = map(value, 540, 1024, 127, 255);
  }
  else if (value <= 450)
  {
    value = map(value, 450, 0, 127, 0);
  }
  else
  {
    value = 127;
  }

  if (reverse)
  {
    value = 254 - value;
  }
  return value;
}

void loop()
{
  data.pot1         = mapAndAdjustJoystickDeadBandValues(analogRead(A0), true);
  data.pot2         = mapAndAdjustJoystickDeadBandValues(analogRead(A1), false);
  Serial.print("The value of pot2 :");
  Serial.println(data.pot2);
  //data.pot3           = map(analogRead(A3), 0, 1023, 0, 254);
  data.swtch1         =!digitalRead(A2);
        
  radio.write(&data, sizeof(packetdata));
}

Remote Controlled self balancing robot