Remote Controlled Self-Balancing Robot
by Aaqib Saad in Circuits > Robots
6538 Views, 91 Favorites, 0 Comments
Remote Controlled Self-Balancing Robot
A self-balancing robot using Arduino nano, mpu-6050, and a cheap 6v DC geared motor. This bot not only stands but also walks using wireless data through nrf24l01. I also added an ultrasound sensor for distance measurement. I used everything convenient, no need for fancy 3D printed models or expensive stepper motors.
Supplies
- 2x arduino nano
- 2x nrf24l01
- 1x L298N motor driver
- 2x Small size breadboard
- Jumper wires
- 2x Dc 6v geared motor
- MPU 6050 Triple Axis Accelerometer and Gyro Breakout
- 1x 10k potentiometer
- 1x 10k resistor
- 6x 18650 lithium batteries
All links are Amazon-affiliated.
Little About Theory
Like an inverted pendulum, a self-balancing robot has its center of gravity above the wheels, making it inherently unstable. The robot needs to continuously adjust its wheels to maintain a 90-degree upright position, similar to how the base of an inverted pendulum must move to keep the pendulum upright.
The robot continuously monitors its tilt angle. If it starts to tip forward, the wheels are driven forward to move the base under the center of gravity, pushing it back into balance. If it tips backward, the wheels move in the opposite direction. The robot keeps adjusting its position to stay balanced. The faster the robot approaches to fall the faster the wheels rotate towards the angle of tilt.
It requires continuous feedback (angle of tilt) and a control system (like a PID controller) to make real-time wheel adjustments to maintain stability. We will use the mpu-6050 sensor for real-time feedback. The MPU-6050 device combines a 3-axis gyroscope and a 3-axis accelerometer on the same silicon die with an onboard Digital Motion Processor (DMP).
To allow the robot to balance, we need to know two key things:
- The current tilt angle of the robot and the desired tilt angle (setpoint).
- The rate of change of this angle (how quickly the robot is tipping over).
To obtain these readings:
- We use the accelerometer to estimate the tilt angle. The accelerometer measures the acceleration due to gravity along different axes, which can be used to calculate the tilt angle relative to the ground.
- We use the gyroscope to measure the angular velocity, which tells us how quickly the robot is rotating or falling over.
By combining data from both the accelerometer and gyroscope, often using a technique like a complementary filter or a Kalman filter, we can obtain a more accurate estimate of the robot's tilt angle and angular velocity. These values are then used in the control system to adjust the speed and direction of the motors to maintain balance.
Here I am using prebuilt library functions that give almost accurate readings.
Making the Body
I have used 8mm pvc board for the frame. You can use plywood, 3d printed Parts or whatever handy stuff you have but make sure it is sturdy. The shape is up to your imagination, but you need to consider the weight distribution, the better it is the less you have to do tuning in the programming section. I have made a boxy shape to keep things simple.
Use image reference for the body making. I used hot glue in the joints. Note: Align the motors to the center as precisely as you can.
Next comes the controversial part, some believe most of the mass should be placed as high as possible, where others say the opposite. My logic is, Yes, it is hard to break the balance when maximum mass is located at the lower but at the same way it is also difficult to regain its balance from a fall. So I am recommend placing most of the mass (generally batteries are the heaviest) at top or middle position although it is not absolutely necessary. I tried both models, both works for my model. The final Choice depends on your model.
Second important thing, It doesn't not matter where you place the mpu 6050. The angle of tilt is always the same no matter where you place it. In simple logic the lower you place the sensor the less you record unnecessary noise. But you can always change the sensitivity of the readings in the coding.
I tried all three position (bottom, middle, top) all perfectly work, just I need to adjust the PID values accordingly. We will discuss about PID latter in programming section.
Connections
MPU 6050 :
PIN NAME = Arduino Pin Number
VCC = 5v
GND = GND
SCL = A4
SDA = A5
nrf24l01 :
PIN NAME = Arduino Pin Number
VCC = 3V3
GND = GND
CE = A0
CSN = A1
SCK = D13
MISO = D12
MOSI = D11
L298N :
PIN NAME = Arduino Pin Number
ENA = 3
IN1 = 4
IN2 = 5
ENB = 10
IN3 = 6
IN4 = 7
It is recommended to solder 0.1 microfarads (uF) ceramic capacitors (104) to both motor pins. This will minimize noise.
If necessary search google by component name for pin diagram.
Installing Libraries
First Install ARDUINO IDE software.
All Libraries- Use this link, and you will find three folders, now paste these folders into "C:\Users\userName\Documents\Arduino\libraries" in this file directory.
lastly, we need the nrf24l01 library. For this open Arduino ide. Then go to sketch>include library>manage library> search for "rf24" and scroll down you will find "RF24 by TMRH20" Install this library.
Checking MPU 6050 Connections
After adding all required libraries. Upload the example sketch (use image reference) and check if the mpu6050 works. After uploading the sketch move the robot and observe the serial monitor. As you move the robot, you will notice that the yaw, pitch, and roll angles are changing.
At this stage, mark at which angle the pitch is negative and positive. This will help you later.
Including Libraries
- 'SPI.h' and 'nRF24L01.h':These libraries handle communication with the nRF24L01 radio module.
- 'RF24.h': Provides a higher-level interface to control the radio.
- 'PID_v1.h': Implements the PID (Proportional-Integral-Derivative) control algorithm, which is crucial for maintaining balance.
- 'I2Cdev.h' and 'MPU6050_6Axis_MotionApps20.h': Used to interface with the MPU6050 sensor to retrieve orientation data.
Setting Up PID Controller
A PID controller helps keep the robot balanced by adjusting the motor output based on sensor readings. It calculates three parts: proportional, integral, and derivative responses, and combines them to control the motors. The goal is to maintain a target angle (setpointPitchAngle/setpointYawAngle). We can also change this target angle remotely to move the robot forward or backward. The difference between the current angle and the target angle is called the error. The PID controller uses this error to determine the speed and direction of the motors. We're using a PID library to simplify this process.
You will find more details on Google.
We set a minimum speed for the motors. Test each motor with a load to find the lowest PWM value where it starts rotating, and set that as the minimum speed. Even if you're using identical motors, this value might differ slightly, so adjust them individually to keep both motors running at the same speed.
For balance, I set the target pitch angle (setpointPitchAngle) to 0 degrees. However, your robot might not balance perfectly at 0 degrees due to factors like weight distribution, sensor placement, or small errors in the sensor itself. You may need to adjust the setpoint through trial and error to find the angle that works best for your robot.
PID Tuning:
Since I am not using encoders to physically ensure the rotation of motors, To keep the robot moving parallelly I used two sets of PID controllers. One fixes the pitch and the other for the yaw. If you set the yaw PID values to zero, the robot can still balance using the pitch PID. However, any slight imbalance or unevenness in the floor might cause the robot to start rotating unexpectedly, leading it to fall since nothing is controlling the yaw movement.
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).
TIP: The KI term integrates the error time so if the robot drifts forward/backward you should increase the PID_PITCH_KI value. You can go up to 5/7 times of PID_PITCH_KP. You can also do this for yaw pid tunning accordingly.
Setting Sensor Offsets
After initializing the MPU 6050, we need to manually set the offset. These offsets are used to correct the readings from the accelerometer and gyroscope to improve accuracy. Each sensor has its offsets even if they are from the same manufacturer. To figure out the offsets, we will average 100 raw readings from the sensor. Don't worry we will not do this by hand. The Library we installed has a premade loop program.
First place the robot at a 90-degree vertical position as accurately as possible and hold it there with some support rather than holding it by hand. Then open Arduino ide>files>examples>MPU6050>IMU_Zero; upload the code and open serial monitor (9600). It will take 4/5 minutes. When finished copy the values and paste the associated values into the program.
Setting Up the Transmitter
#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;
First, we include the necessary libraries and define CE and CSN pins to create rf24 object. CE and CSN pins are used to trigger the radio module to work as either transmitter or receiver.
We defined an address (const byte address[6] = "ABCDE") that acts like a password. This address must be the same on both the transmitter and receiver for successful communication.
Next, we created a data package with four 1-byte variables: "pot1," "pot2," "pot3," and "switch1," and named it "data." On the receiver side, we named it "receiverdata."
To access a variable on the transmitter side, we use data.pot1 for "pot1" and data.pot2 for "pot2." On the receiver side, we use receiverdata.pot1 and receiverdata.pot2 in the same way.
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);
}
Now we initialize the radio module and start communicating through the predefined address radio.openWritingPipe(address).
he NRF module has two power modes: MAX and MIN. With maximum power setting you will get an extended range than the minimum.
Note: Nrf 24l01 is sensitive to the current supply. The Arduino's built-in 3.3v regulator may not provide sufficient current. I recommend using a separate 3.3v voltage regulator and a 100uf electrolyte capacitor between the power lines for a smooth current supply.
I made my adapter board with an ams1117 voltage regulator using Veroboard. You can also find it online with a built-in 3.3V voltage regulator.
nRF24L01+ supports an air data rate of 250 kbps, 1 Mbps, and 2 Mbps. For our project, 250 kbps (radio.setDataRate(RF24_250KBPS)) speed is more than sufficient. Remember: The higher the data rate lower the range you will get.
nRF24L01+ can operate on 125 different channels from 0 to 124. We set the channel to "124" (radio.setChannel(124)). It is the most stable channel. You will find more details about channels on google.
The next part is interesting. The radio module can only speak(transmit) or listen(receive). When we tell it to stop listening (radio.stopListening() ) it means start speaking or set the mode as transmitter
int mapAndAdjustJoystickDeadBandValues(int value, bool reverse)
{
if (value >= 580)
{
value = map(value, 580, 1024, 127, 255);
}
else if (value <= 400)
{
value = map(value, 400, 0, 127, 0);
}
else
{
value = 127;
}
if (reverse)
{
value = 254 - value;
}
return value;
}
The cheap analog joystick that I am using does not provide accurate reading every time. So I trimmed the reading using _ mapAndAdjustJoystickDeadBandValues this function. It will overlook slight irregularities at the center position reading and always give 127 as the center position.
void loop()
{
data.pot1 = mapAndAdjustJoystickDeadBandValues(analogRead(A0), false);
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));
}
In the void loop section, we pass the raw analog reading through the mapAndAdjustJoystickDeadBandValues function for filtering. and transmit the data using radio.write function.
For now, we only need "data.pot1" and "data.pot2" variables to control the robot's movement; and "data.swtch1" is used to toggle the ultrasound on and off. "data.pot3" has been added in case we need to attach other things in the future.
Downloads
Wireless Movement Control
This part controls the robot’s pitch angle. The pitch PID is always active to maintain the desired angle, which defaults to 0. When the radio is working, it adjusts the pitch angle based on the potentiometer value. If the radio stops working, it resets the pitch angle to 0 and turns off the LED to show that the radio connection is lost.
if (radio.available()) {
radio.read(&receiverdata, sizeof(packetdata));
lastSignalTime = millis();
digitalWrite(LED, HIGH); // Signal received LED indicator
// Adjust pitch setpoint based on pot1 value
if (receiverdata.pot1 == 127) {
setpointPitchAngle = 0; // Center position
} else {
setpointPitchAngle = map(receiverdata.pot1, 0, 255, -2500, 2500) / 1000.0;
}
if (receiverdata.pot2 != 127) {
yawPIDOutput = 0; // Stop yaw PID control output
motorAdjustment = map(receiverdata.pot2, 0, 255, -200, 200); // Directly adjust motors, increase or decrease this value as per your need.
} else {
yawPID.Compute(); // Continue with yaw PID control if pot2 is centered
motorAdjustment = 0; // Reset manual adjustment
}
} else if (millis() - lastSignalTime > signalTimeout) {
setpointPitchAngle = 0;
motorAdjustment = 0;
yawPID.Compute();
digitalWrite(LED, LOW); // Signal loss indicator
}
The robot dynamically switches between automated yaw control using the PID controller and manual yaw adjustments based on the radio's input from pot2.
When the radio is available and pot2 is centered (127): The yaw PID controller is used to stabilize the robot's yaw based on the gyroscope data.
When the radio is available but pot2 is not centered: The yaw PID controller is disabled, and the yaw control is done manually by adjusting the motor speed based on the pot2 value.
When the radio is not available: The yaw PID controller remains active to maintain the robot's yaw stability autonomously.
NOTE: Even though the yaw PID is disabled for rotation, the pitch PID is still running and trying to maintain balance.
If the robot is attempting to rotate left or right and the pitch PID detects an imbalance, it might counteract the intended rotation by adjusting the motor speeds. This behavior can create a situation where the pitch PID output is large enough to overpower the manual motor adjustment, causing the robot not to rotate as intended or appear to "freeze".
If you encounter this problem, first check the side of the transmitter using serial print to see if it is physically reading the correct values. If not check for loose connections or consider replacing the joystick. If this is still not resolved, simply increase the motor adjustment value. In my case, I used 200.
Adding Ultrasound
I used 3mm PVC board and double-sided tape to attach the ultrasound sensor to the body.
I made many failed attempts to integrate ultrasound within the main program. Check out this Arduino forum thread you will see (from post#30 and onward) each failure approach in detail. In brief, adding an ultrasound makes the loop slower and consequently the robot is unable to balance and maintain stable radio connectivity. The program is reading the data from the mpu6050 and accordingly sets the speed and direction of the motor at a very high frequency. So the slow loop crashes the coordination between each element.
You may try faster IMU and faster Arduino. For me, after completing this much it is not financially suitable to replace the Arduino Nano with a faster model.
To come up with a solution I assign the ultrasound part to an attiny85. The Attity85 is programmed to measure the distance and digital write to pin 4 if an object is detected in the desired detection range. I used a potentiometer to adjust the object detection range from 0 to 100 cm.
I have used a delay of 300 milliseconds you can change this value according to your need. With a shorter pulse, you may need to set the setpoint pitch angle higher to counteract a detected obstacle.
To be honest, deligating the ultrasound part into a separate microcontroller makes things a lot easier than integrating RADIO, MPU, PID, and ULTRASOUND altogether in one single program without slowing down the execution time of the whole program. Now that we have one single variable. If the ultrasound detects an object attiny85 will send a high pulse using digitalWrite to the Arduino nano, and then the nano will receive the pulse as 1 and 0 if no object is detected.
You can use this variable to do whatever function you like, with no slower loop/timing issue.
I used receiverdata.switch1 (built-in joystick push button) to toggle between modes with and without ultrasound protection when radio data is available. By default, ultrasound protection is on. A press of the push button on the transmitter side turns the ultrasound protection off and on again.
NOTE: Use a 10k pull-down resistor via the A2 and GND pin. This is a must. otherwise, we may not get proper digitalRead. Also don't use any led directly to the attiny output pin. For me, this hampers the digitalRead function. I'm not sure why this happened. But after removing the LED it works fine.
As this program is very small you may use other versions of attiny with small storage capacity. All versions have the same processing capabilities but the storage space varies.
Downloads
Final Code and Final Thoughts
In the whole program, PID tuning is the painstaking part. Do some research on Google. You will find lots of tips on how to fine-tune PID. It takes time and a lot of trial and error. So, Just be patient. And be careful with the Serial print function. Using unnecessary serial print at a higher baud rate will make your loop slower. So, be careful with serial print, use a lower baud rate like 9600. In my experiment when I commented/removed serial print from the program I needed to readjust my PID values as the loop becomes faster, and in some cases I used a small delay to compensate for that without changing PID values.
Secondly, The reason for using separate batteries is because the motor voltage creates noise (back emf) on the voltage line which can cause incorrect readings from the MPU6050. In the final PCB layout, I'll be using separate voltage rails for the Arduino and the motor drivers which I can't test using a breadboard at the moment.
For the motor driver, you may use classic l293d. For me l298n was handy.
Currently, it only moves backward when it encounters an obstacle. In the future, I will try to make it go forward while avoiding obstacles. Next, I plan to use infrared LEDs to track an object. something like "self-balancing line following robot"
Finally, when all fine-tuning is complete I don't need the whole Arduino board anymore. I will upload the program in a bare atmega 328p microcontroller just like attiny85, this will make my final circuit tidy and impressive. There will be no wire mess in the final circuit.
HAPPY BALANCING...
Attached: 1) Balanced_Robot_16 - simple movement control through radio, without ultrasound
2) Balanced_Robot_22 - Ultrasound integration along radio-based movement control