arduino-gimbal-estabilizador-camara

How to Build a Two-Axis Gimbal with Arduino

  • 8 min

A two-axis gimbal is a motorized mount that compensates for pitch and roll tilts to keep a load roughly leveled.

You’ve probably seen those smooth drone or action camera videos that seem to float in the air. That’s achieved with a Gimbal. Its job is to read the mount’s inclination and move motors in the opposite direction by exactly the same number of degrees so the camera always remains level.

In this tutorial, we’ll build one using standard servo motors. Although professional gimbals use Brushless motors (which are faster and smoother), using servos is the best way to understand vector math and PID control without getting bogged down with expensive three-phase drivers.

This project reuses key knowledge from the Self-Balancing Robot: IMU reading, sensor fusion, and control loops. If you missed that article, check it out first.

Pitch, Roll, and Yaw

To stabilize an object in 3D space, we must control its three rotation axes (Euler Angles):

  1. Pitch: Forward/backward tilt (Y-axis).
  2. Roll: Left/right lateral tilt (X-axis).
  3. Yaw: Rotation around its own vertical axis (Z-axis).

In this project, we’ll make a two-axis gimbal (pitch and roll). We’ll leave out yaw to simplify the mechanics and control; a third axis can also have limited travel depending on the design.

The logic is:

If the handle tilts +10º, the servo must rotate -10º to compensate.

Components

  • [1x|blue] Arduino Nano | Compact, ideal for mounting on the gimbal
  • [1x|orange] MPU6050 Sensor | Accelerometer + Gyroscope {I2C}
  • [2x|green] Servo Motors | SG90 (light) or MG996R (strong)
  • [1x|red] Regulated External Power Supply | 5V or the servos’ nominal voltage
  • [Various|gray] 2-Axis Structure | 3D print (search “2 axis servo gimbal”)

The mechanical structure is critical. We need a mount that allows two axes to move orthogonally. You can 3D print parts (search “2 axis servo gimbal” on Thingiverse) or join two U-shaped plastic brackets.

Wiring Diagram

In this direct control setup, the MPU6050 must be attached to the handle or fixed frame, not the camera. This way, we measure the disturbance we want to compensate for. If we place the IMU on the moving part, we would need a feedback controller that updates the command based on error, not the direct assignment used here.

MPU6050 Module

The MPU6050 communicates via I2C. Check if your module supports 5V or if you need to power it with 3.3V.

[ { “from”: “MPU6050 Module VCC”, “to”: “Arduino 5V”, “color”: “red”, “note”: “Only if the module supports 5V” }, { “from”: “MPU6050 GND”, “to”: “Arduino GND”, “color”: “black” }, { “from”: “MPU6050 SDA”, “to”: “Pin A4”, “color”: “blue”, “note”: “I2C Bus” }, { “from”: “MPU6050 SCL”, “to”: “Pin A5”, “color”: “purple”, “note”: “I2C Bus” } ]

Gimbal Servos

The servos correct pitch and roll. Power them from an external source if the camera or mount is heavy.

[ { “from”: “Pitch Servo (Y-axis) signal”, “to”: “Pin 9”, “color”: “green” }, { “from”: “Roll Servo (X-axis) signal”, “to”: “Pin 10”, “color”: “green” }, { “from”: “Servos VCC”, “to”: “External Power Supply”, “color”: “red”, “note”: “NOT from Arduino” }, { “from”: “Common GND”, “to”: “Arduino GND + Power Supply”, “color”: “black”, “note”: “Mandatory common ground” } ]

The Code

We’ll implement direct control with smoothing. Although a full PID is ideal, for servos (which already have their own internal control electronics) accurate angle calculation and good filtering are usually sufficient.

We’ll use the Wire.h library for the sensor and Servo.h for the motors.

Variables and Setup

#include <Wire.h>
#include <Servo.h>

Servo servoPitch;
Servo servoRoll;

// Pin constants
const int pinPitch = 9;
const int pinRoll = 10;

// MPU6050 variables
int16_t accX, accY, accZ;
int16_t gyroX, gyroY, gyroZ;

// Calculated angles
float accAngleX, accAngleY;
float gyroAngleX, gyroAngleY;
float currentAngleX, currentAngleY;

// Time variables for the filter
unsigned long elapsedTime, currentTime, previousTime;
float dt;

// Calibration (Offset)
// Adjust these values so the camera points forward on startup
int pitchOffset = 90; 
int rollOffset = 90;

void setup() {
  Serial.begin(9600);
  
  // Initialize MPU6050
  Wire.begin();
  Wire.beginTransmission(0x68);
  Wire.write(0x6B); 
  Wire.write(0x00); // Wake up sensor
  Wire.endTransmission(true);

  servoPitch.attach(pinPitch);
  servoRoll.attach(pinRoll);
  
  // Safe initial position
  servoPitch.write(pitchOffset);
  servoRoll.write(rollOffset);
  
  previousTime = millis();
}
Copied!

Reading and Complementary Filter

Inside the loop, we read raw data and fuse the accelerometer (stable but slow) with the gyroscope (fast but drifts).

void loop() {
  // 1. Time management (Delta Time)
  currentTime = millis();
  dt = (currentTime - previousTime) / 1000.0;
  previousTime = currentTime;

  if (dt <= 0 || dt > 0.1) return;

  // 2. Read MPU data
  readMPU();

  // 3. Calculate angle with Accelerometer (Trigonometry)
  // Convert gravity vectors into angles
  accAngleX = (atan(accY / sqrt(pow(accX, 2) + pow(accZ, 2))) * 180 / PI);
  accAngleY = (atan(-1 * accX / sqrt(pow(accY, 2) + pow(accZ, 2))) * 180 / PI);

  // 4. Calculate angle with Gyroscope (Integration)
  // gyroX/131.0 converts the raw reading to degrees/second
  float gyroRateX = gyroX / 131.0;
  float gyroRateY = gyroY / 131.0;

  // 5. Complementary Filter
  // We trust the gyroscope 96% and the accelerometer 4% to correct drift
  currentAngleX = 0.96 * (currentAngleX + gyroRateX * dt) + 0.04 * accAngleX;
  currentAngleY = 0.96 * (currentAngleY + gyroRateY * dt) + 0.04 * accAngleY;

  // 6. Move Servos
  moverGimbal();
}

void readMPU() {
  Wire.beginTransmission(0x68);
  Wire.write(0x3B); // Starting register
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 6, true);
  
  accX = (Wire.read() << 8 | Wire.read());
  accY = (Wire.read() << 8 | Wire.read());
  accZ = (Wire.read() << 8 | Wire.read());
  
  // Skip temperature
  Wire.beginTransmission(0x68);
  Wire.write(0x43); // Gyroscope register
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 6, true);
  
  gyroX = (Wire.read() << 8 | Wire.read());
  gyroY = (Wire.read() << 8 | Wire.read());
  gyroZ = (Wire.read() << 8 | Wire.read());
}
Copied!

Correction Logic

Here we translate the tilt angle into servo movement.

  • If the sensor detects the camera is at +20º (tilted right), the servo should move to 90 - 20 = 70º to straighten it.
void moverGimbal() {
  // X-AXIS (Roll)
  // Map and correct. 
  // Note: Servos range from 0 to 180.
  // You may need to change the sign (+) to (-) depending on how you mount the servo.
  int servoPosRoll = rollOffset + currentAngleX; 
  
  // Y-AXIS (Pitch)
  int servoPosPitch = pitchOffset - currentAngleY;

  // Constrain to avoid forcing mechanics
  servoPosRoll = constrain(servoPosRoll, 0, 180);
  servoPosPitch = constrain(servoPosPitch, 0, 180);

  servoRoll.write(servoPosRoll);
  servoPitch.write(servoPosPitch);
  
  // Debug (optional)
  /*
  Serial.print("Roll: "); Serial.print(currentAngleX);
  Serial.print(" | Pitch: "); Serial.println(currentAngleY);
  */
}
Copied!

Output Smoothing

If you use the code above, you’ll see it works, but the servos might “jitter” a bit. This is because the accelerometer is very sensitive.

To fix this, we can apply exponential smoothing to the servo output.

Replace the moverGimbal function with this improved version:

float smoothRoll = 90;
float smoothPitch = 90;
float alpha = 0.1; // Smoothing factor (0.1 = very smooth/slow, 0.9 = fast)

void moverGimbal() {
  float targetRoll = constrain(rollOffset + currentAngleX, 0, 180);
  float targetPitch = constrain(pitchOffset - currentAngleY, 0, 180);
  
  // Low-pass filter on the output
  smoothRoll = (alpha * targetRoll) + ((1.0 - alpha) * smoothRoll);
  smoothPitch = (alpha * targetPitch) + ((1.0 - alpha) * smoothPitch);
  
  servoRoll.write((int)smoothRoll);
  servoPitch.write((int)smoothPitch);
}
Copied!

Limitations and Evolution

You’ve built a functional gimbal, congratulations! But you’ll notice its limits:

  1. Speed: Servos have gears. They aren’t instantaneous. If you shake the gimbal very fast, the camera will be slow to react.
  2. Noise: Digital servos make constant noise when correcting.

Brushless Gimbal Motors

Professional gimbals (like DJI or Zhiyun) use Brushless Gimbal Motors.

  • They have no gears (Direct Drive) -> Zero backlash.
  • They are silent and ultra-fast.
  • The problem: They cannot be controlled directly with Arduino. You need complex controllers with three H-bridges and Field-Oriented Control (FOC) algorithms.

Before using the assembly with a camera, calibrate the gyroscope bias with the structure stationary and limit the angles according to the mechanics. The example serves for experimentation but does not offer the smoothness or precision of a commercial gimbal.