Ok, before we start, I have to say that this is far from the best gimbal project. But I hope this tutorial will at least teach you something about how gimbals work and how to build one. You will ahve all the schematics, codes and 3D files if you want to improve this project yourself. What I wanted is to make a project based on a Tarot commercial gimbal that also uses servos. But for now, the speed of this servos is too slow so I should use bigger
by: ELECTRONOOBS on 2026-10-05
1 x Arduino NANO: LINK eBay
1 x MPU6050: LINK eBay
1 x Buck converter: LINK eBay
2 x Futaba S3003 servos: LINK eBay
1 x GT2 belt: LINK eBay
1 x 11.1V LiPo: LINK eBay
6 x 22mm bearings: LINK eBay
1 x M8 250mm: LINK eBay
15 x M8 nuts: LINK eBay
40 M3x30mm screw: LINK eBay
40 M3x30mm nut: LINK eBay
M3 Washers: LINK eBay
20mm PVC tube: Local store
3D STL parts: See here
Wire, soldering iron, solder, etc...

This is what we need for this. I’ve used some 2cm PVC tubes which probably aren’t the best choice. You have the full part list and the dimensions in the link above. We also need all the 3D printed files. There are a lot of printed files. Using 3 printers at the same time, it took me around 10 hours to print them all. Download and print each one. I’ve used PLA material, 0.4mm nozzle, 0.2mm layer height and a perimeter of 2 layers. We have plastic joints, gears, brackets and so on.


We also need 3 bits of 8mm screws and some nuts as well. 6 22mm bearings and two servo motors. I’ve used the futaba S3003 servo motors since these have quite some torque. We will also need some GT2 belts and probably some glue. And that’s it for the frame. So, let’s begin. I first mount the X axis of the gimbal.

For that we need a 20cm long PVC tube with an 8mm hole exactly in the middle. Then we need the two printed plastic couplers and two more PCV tubes of 9cm. Now that we have the parts we start by adding the 3mm screws and washers to the plastic couplers. Don’t tight them too much. Now we take the 20cm long PVC tube and insert it into the plastic coupler follower by the two other smaller tubes. Make sure that the hole is in the middle and parallel to the smaller tubes and then you could tight the nuts.

Now all is there to do is to add the big gear and the bearings. As in the photo below, first tight strong the nuts on an 8mm screw and the big 3D printed gear. Add the bearings to the support we have just amde annd then tight the screws. Now, you should add the swing 3D printed aprds and add anoter PVC tube and make sure it can rotate with no friction.

For the "Y-axis" just make the shape of a T with the 3D printed parts and the PVC tube. Now all we have to do is to join these ttwo aprts together and the gimbal should be ready. For that we will need the second 3D printed big gear and some more bearings.

Finnaly, we join the parts together and we finish the frame. Add the bearings, add the 8mm screw and tight the nuts on the 3D printed gear. To be ture it won't slip, I've placed a bit of hot glue to the joints. THat's it, make sure each axis could move with no problem.

This is the scheamtic for the gimbal belwo. We haev the Arduino that will control everything. The MPU6050 gyro/acc module that will measure the angle. Then we have 2 servo motors to move the axis. Also, very important, there must be a 5V extrenal voltage regulator. The Arduino has a 5V regulator but that can't provide enough current for the motors. That's why we use the external one set to 5V output.

I place the IMU on the bottom tube right next to the camera support. Now we can detect the angle of the gimbal. I've palced the Arduino on the side next to the voltage regulator. I connect both motors to the Arduino pins. Tight the belts and it's time for the code.

The code is not that complicated. This is how it works. The gimbal moves, the IMU detects the angle change and it changes the PID values. The PID is used to fast react at the angle change. The PID will change the PWM width of the signals connected to the motors in order to keep the same angle. Usually, gimbals have a joystick to change the desired angle. In this case the desired angle will be always 0 till future versions.
/* http://www.youtube.com/c/electronoobs
*
* This is an example for the 3D printed gimbal project. It uses
* the MPU6050 and two S3003 servo motors. This is the PID code for the gimbal. *
* Arduino pin | MPU6050
* 5V | Vcc
* GND | GND
* A4 | SDA
* A5 | SCL
* Scheematic: https://www.electronoobs.com/eng_arduino_tut46_sch1.php
*/
//Includes
#include <Wire.h>
#include <Servo.h>
Servo pitch;
Servo roll;
/*MPU-6050 gives you 16 bits data so you have to create some float constants
*to store the data for accelerations and gyro*/
//Gyro Variables
float elapsedTime, time, timePrev; //Variables for time control
int gyro_error=0; //We use this variable to only calculate once the gyro data error
float Gyr_rawX, Gyr_rawY, Gyr_rawZ; //Here we store the raw data read
float Gyro_angle_x, Gyro_angle_y; //Here we store the angle value obtained with Gyro data
float Gyro_raw_error_x, Gyro_raw_error_y; //Here we store the initial gyro data error
//Acc Variables
int acc_error=0; //We use this variable to only calculate once the Acc data error
float rad_to_deg = 180/3.141592654; //This value is for pasing from radians to degrees values
float Acc_rawX, Acc_rawY, Acc_rawZ; //Here we store the raw data read
float Acc_angle_x, Acc_angle_y; //Here we store the angle value obtained with Acc data
float Acc_angle_error_x, Acc_angle_error_y; //Here we store the initial Acc data error
float Total_angle_x, Total_angle_y; //Here we store the final total angle
//More variables for the code...
int i;
int mot_activated=0;
long activate_count=0;
long des_activate_count=0;
//////////////////////////////PID FOR ROLL///////////////////////////
float roll_PID, pwm_L_F, pwm_L_B, pwm_R_F, pwm_R_B, roll_error, roll_previous_error;
float roll_pid_p=0;
float roll_pid_i=0;
float roll_pid_d=0;
///////////////////////////////ROLL PID CONSTANTS////////////////////
double roll_kp=4;//3.55
double roll_ki=0.1;//0.003
double roll_kd=0.1;//2.05
float roll_desired_angle = 0; //This is the angle in which we whant the
//////////////////////////////PID FOR PITCH//////////////////////////
float pitch_PID, pitch_error, pitch_previous_error;
float pitch_pid_p=0;
float pitch_pid_i=0;
float pitch_pid_d=0;
///////////////////////////////PITCH PID CONSTANTS///////////////////
double pitch_kp=4;//3.55
double pitch_ki=0.1;//0.003
double pitch_kd=0.1;//2.05
float pitch_desired_angle = 0; //This is the angle in which we want the gimbal to stay (for now it will be 0) Joystick for future versions
float PWM_pitch, PWM_roll;
void setup() {
pitch.attach(5); //servo motor for pitch
roll.attach(3); //servo motor for roll
Wire.begin();
//begin the wire comunication
Wire.beginTransmission(0x68); //begin, Send the slave adress (in this case 68)
Wire.write(0x6B); //make the reset (place a 0 into the 6B register)
Wire.write(0x00);
Wire.endTransmission(true); //end the transmission
//Gyro config
Wire.beginTransmission(0x68); //begin, Send the slave adress (in this case 68)
Wire.write(0x1B); //We want to write to the GYRO_CONFIG register (1B hex)
Wire.write(0x10); //Set the register bits as 00010000 (1000dps full scale)
Wire.endTransmission(true); //End the transmission with the gyro
//Acc config
Wire.beginTransmission(0x68); //Start communication with the address found during search.
Wire.write(0x1C); //We want to write to the ACCEL_CONFIG register
Wire.write(0x10); //Set the register bits as 00010000 (+/- 8g full scale range)
Wire.endTransmission(true);
//Serial.begin(9600); //Remember to set this same baud rate to the serial monitor
time = millis(); //Start counting time in milliseconds
}//end of setup void
void loop() {
/////////////////////////////I M U/////////////////////////////////////
timePrev = time; // the previous time is stored before the actual time read
time = millis(); // actual time read
elapsedTime = (time - timePrev) / 1000;
/*The tiemeStep is the time that elapsed since the previous loop.
*This is the value that we will use in the formulas as "elapsedTime"
*in seconds. We work in ms so we have to divide the value by 1000
to obtain seconds*/
/*Reed the values that the accelerometre gives.
* We know that the slave adress for this IMU is 0x68 in
* hexadecimal. For that in the RequestFrom and the
* begin functions we have to put this value.*/
//////////////////////////////////////Gyro read/////////////////////////////////////
Wire.beginTransmission(0x68); //begin, Send the slave adress (in this case 68)
Wire.write(0x43); //First adress of the Gyro data
Wire.endTransmission(false);
Wire.requestFrom(0x68,4,true); //We ask for just 4 registers
Gyr_rawX=Wire.read()<<8|Wire.read(); //Once again we shif and sum
Gyr_rawY=Wire.read()<<8|Wire.read();
/*Now in order to obtain the gyro data in degrees/seconds we have to divide first
the raw value by 32.8 because that's the value that the datasheet gives us for a 1000dps range*/
/*---X---*/
Gyr_rawX = (Gyr_rawX/32.8);
/*---Y---*/
Gyr_rawY = (Gyr_rawY/32.8);
/*Now we integrate the raw value in degrees per seconds in order to obtain the angle
* If you multiply degrees/seconds by seconds you obtain degrees */
/*---X---*/
Gyro_angle_x = Gyr_rawX*elapsedTime;
/*---X---*/
Gyro_angle_y = Gyr_rawY*elapsedTime;
//////////////////////////////////////Acc read/////////////////////////////////////
Wire.beginTransmission(0x68); //begin, Send the slave adress (in this case 68)
Wire.write(0x3B); //Ask for the 0x3B register- correspond to AcX
Wire.endTransmission(false); //keep the transmission and next
Wire.requestFrom(0x68,6,true); //We ask for next 6 registers starting withj the 3B
/*We have asked for the 0x3B register. The IMU will send a brust of register.
* The amount of register to read is specify in the requestFrom function.
* In this case we request 6 registers. Each value of acceleration is made out of
* two 8bits registers, low values and high values. For that we request the 6 of them
* and just make then sum of each pair. For that we shift to the left the high values
* register (<<) and make an or (|) operation to add the low values.
If we read the datasheet, for a range of+-8g, we have to divide the raw values by 4096*/
Acc_rawX=(Wire.read()<<8|Wire.read())/4096.0 ; //each value needs two registres
Acc_rawY=(Wire.read()<<8|Wire.read())/4096.0 ;
Acc_rawZ=(Wire.read()<<8|Wire.read())/4096.0 ;
/*Now in order to obtain the Acc angles we use euler formula with acceleration values
after that we substract the error value found before*/
/*---X---*/
Acc_angle_x = (atan((Acc_rawY)/sqrt(pow((Acc_rawX),2) + pow((Acc_rawZ),2)))*rad_to_deg) ;
/*---Y---*/
Acc_angle_y = (atan(-1*(Acc_rawX)/sqrt(pow((Acc_rawY),2) + pow((Acc_rawZ),2)))*rad_to_deg) ;
//////////////////////////////////////Total angle and filter/////////////////////////////////////
/*---X axis angle---*/
Total_angle_x = 0.98 *(Total_angle_x + Gyro_angle_x) + 0.02*Acc_angle_x;
/*---Y axis angle---*/
Total_angle_y = 0.98 *(Total_angle_y + Gyro_angle_y) + 0.02*Acc_angle_y;
//Uncomment this below for debug
/*
Serial.print("GyroX angle: ");
Serial.print(Total_angle_x);
Serial.print(" | ");
Serial.print("GyroY angle: ");
Serial.println(Total_angle_y);*/
/*///////////////////////////P I D///////////////////////////////////*/
roll_desired_angle = 0; //The angle we want the gimbal to stay is 0 and 0 for both axis for now...
pitch_desired_angle = 0;
/*First calculate the error between the desired angle and
*the real measured angle*/
roll_error = Total_angle_y - roll_desired_angle;
pitch_error = Total_angle_x - pitch_desired_angle;
/*Next the proportional value of the PID is just a proportional constant
*multiplied by the error*/
roll_pid_p = roll_kp*roll_error;
pitch_pid_p = pitch_kp*pitch_error;
/*The integral part should only act if we are close to the
desired position but we want to fine tune the error. That's
why I've made a if operation for an error between -2 and 2 degree.
To integrate we just sum the previous integral value with the
error multiplied by the integral constant. This will integrate (increase)
the value each loop till we reach the 0 point*/
if(-3 < roll_error <3)
{
roll_pid_i = roll_pid_i+(roll_ki*roll_error);
}
if(-3 < pitch_error < 3)
{
pitch_pid_i = pitch_pid_i+(pitch_ki*pitch_error);
}
/*The last part is the derivate. The derivate acts upon the speed of the error.
As we know the speed is the amount of error that produced in a certain amount of
time divided by that time. For taht we will use a variable called previous_error.
We substract that value from the actual error and divide all by the elapsed time.
Finnaly we multiply the result by the derivate constant*/
roll_pid_d = roll_kd*((roll_error - roll_previous_error)/elapsedTime);
pitch_pid_d = pitch_kd*((pitch_error - pitch_previous_error)/elapsedTime);
/*The final PID values is the sum of each of this 3 parts*/
roll_PID = roll_pid_p + roll_pid_i + roll_pid_d ;
pitch_PID = pitch_pid_p + pitch_pid_i + pitch_pid_d ;
/*We know taht the min value of PWM signal is -90 (usingservo.write) and the max is 90. So that
tells us that the PID value can/s oscilate more than -90 and 90 so we constrain those values below*/
if(roll_PID < -90){roll_PID = -90;}
if(roll_PID > 90) {roll_PID = 90; }
if(pitch_PID < -90){pitch_PID = -90;}
if(pitch_PID > 90) {pitch_PID = 90;}
roll_previous_error = roll_error; //Remember to store the previous error.
pitch_previous_error = pitch_error; //Remember to store the previous error.
PWM_pitch = 90 + pitch_PID; //Angle for each motor is 90 plus/minus the PID value
PWM_roll = 90 - roll_PID;
pitch.write(PWM_pitch); //Finally we write the angle to the servos
roll.write(PWM_roll);
}//end of void loop
I map the values to a range between -90 and 90 degrees. But you could change that value. Also, my gimbal had a maximum of -/+20 degrees for thr X axis and -/+50 degrees for the y axis. Then we write these values using the analogWrite() function and control the servos.
Leave a comment
Please login in order to comment.