Tilt and Motion with an MPU-6050

Draft. This tutorial has not been checked against the physical product yet. Confirm wiring, pin assignments and voltage limits against the product documentation before building it.
DifficultyAdvanced
CategorySensors
Supported boardsArduino Uno R3 and compatible ATmega328P boards
Time needed65 minutes
PrerequisitesI2C, Two Wires and Many Devices
Tested boardNot yet verified
Tested onNot yet verified
Arduino IDE versionNot yet verified
LibraryNot yet verified

What it does

Six sensors in one chip: a three-axis accelerometer and a three-axis gyroscope, on the GY-521 board.

The accelerometer measures acceleration, and because gravity is an acceleration, a stationary sensor tells you which way is down. That is how it knows tilt.

The gyroscope measures rate of rotation, in degrees per second. It says nothing about which way you are pointing, only how fast that is changing.

Neither alone is much good, and understanding why is the whole point of this tutorial. The accelerometer is steady but noisy and confused by any real movement. The gyroscope is smooth and responsive but drifts away from the truth within seconds. Put them together properly and you get an angle you can trust, which is what every drone, phone and self-balancing robot does.

Note that the product title calls this a 3-axis gyroscope. It is both: six axes in total.

Before you begin

Required parts and tools

  • An Arduino Uno or compatible board.
  • One GY-521 MPU-6050 module. This is Siratek product 2929.
  • Four jumper wires.

The board

The silkscreen reads VCC GND SCL SDA XDA XCL ADO INT, eight pins, of which you need four.

The board carries a voltage regulator and a power LED, so VCC takes 5 V from an Uno and the chip gets its 3.3 V. The I2C pull-ups on the board go to that 3.3 V rail, which an Uno reads as HIGH without trouble. This is the common arrangement and it works; a 3.3 V board such as an ESP32 avoids the question entirely.

XDA and XCL are a second I2C bus for attaching a magnetometer. INT signals that new data is ready, which matters when you need every sample and not before.

Address 0x68, which it shares with the clock

The MPU-6050 answers at 0x68. So does the DS3231.

Put both on one bus and neither works properly, and the symptom is confusing because each is fine on its own.

The fix is on the board: tie ADO to 3.3 V and the MPU-6050 moves to 0x69. Then both live happily together, which a self-levelling datalogger needs.

Run the bus scanner before building anything with two devices. It takes thirty seconds and saves an afternoon.

What the raw numbers are

getAcceleration and getRotation return raw 16-bit counts, not g and not degrees per second. Converting needs the full-scale range the chip is set to:

Accelerometer range Counts per g
+/- 2 g, the default 16384
+/- 4 g 8192
+/- 8 g 4096
+/- 16 g 2048
Gyroscope range Counts per degree/second
+/- 250 deg/s, the default 131
+/- 500 deg/s 65.5
+/- 1000 deg/s 32.8
+/- 2000 deg/s 16.4

At the default +/- 2 g, a flat sensor reads about 16384 on its Z axis and near zero on X and Y. Seeing 16384 is how you confirm it is working and the right way up.

Why gyro drift is unavoidable

A gyroscope measures rate. To get an angle you add up the rate over time, and every small error in every reading is added in too and never removed.

So a gyro-derived angle is excellent for a second and visibly wrong after a minute. This is not a cheap-sensor problem; it is what integration does. Expensive gyroscopes drift more slowly, not differently.

The accelerometer has the opposite character: it always knows which way gravity points, so it never drifts, but any movement of the sensor adds to the gravity vector and corrupts the reading. Tap the board and the accelerometer angle jumps wildly while the gyro stays smooth.

The complementary filter takes the gyro for the short term and the accelerometer for the long term, in one line:

angle = 0.98 * (angle + gyroRate * dt) + 0.02 * accelerometerAngle

The gyro provides the movement; the small accelerometer term quietly pulls the result back towards the truth so the drift never accumulates. It is not the most sophisticated approach, and for a tilt sensor it is entirely sufficient.

Pin reference

Module pin Connect to Purpose Notes
VCC Uno 5V Power The board regulates it
GND Uno GND Ground
SCL Uno A5 I2C clock
SDA Uno A4 I2C data
ADO Not connected, or 3.3V Address select 3.3 V moves it to 0x69
INT Not used here Data ready
A GY-521 MPU-6050 module wired to an Uno over I2C, with a note that tying ADO high moves it off the address the DS3231 also uses
Four wires on the I2C bus at 0x68, which the DS3231 also uses. ADO to 3.3 V moves this one to 0x69.

Wiring steps

  1. Unplug the USB cable.
  2. GND to GND, VCC to 5V.
  3. SDA to A4, SCL to A5.
  4. If a DS3231 is also on the bus, run a wire from ADO to 3.3V.
  5. Plug the USB cable back in and scan the bus. Expect 0x68, or 0x69 if you tied ADO high.

Software and libraries

Install MPU6050 by Electronic Cats through Library Manager, which also pulls in I2Cdevlib. Several libraries share this name; record which one you used.

Minimal working example

// Siratek: raw values from an MPU-6050. Lie it flat, then tilt it.

#include <Wire.h>
#include <MPU6050.h>

MPU6050 mpu;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  mpu.initialize();

  if (!mpu.testConnection()) {
    Serial.println("Siratek: no MPU-6050. Scan the bus; check ADO if a clock is fitted.");
    while (1) {
      delay(100);
    }
  }

  Serial.println("Siratek: flat should give ax and ay near zero, az near 16384.");
}

void loop() {
  int16_t ax, ay, az, gx, gy, gz;
  mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);

  Serial.print("accel ");
  Serial.print(ax); Serial.print(" ");
  Serial.print(ay); Serial.print(" ");
  Serial.print(az);
  Serial.print("    gyro ");
  Serial.print(gx); Serial.print(" ");
  Serial.print(gy); Serial.print(" ");
  Serial.println(gz);

  delay(200);
}

Lie the board flat and check az reads somewhere near 16384. Stand it on each edge in turn and a different axis should take that value. That is the quickest way to learn which axis is which on your board, and the orientation is printed on the silkscreen if you prefer to read it.

The gyro values should sit near zero when still, though not exactly: every MPU-6050 has an offset, which the next sketch deals with.

Tilt from the accelerometer

// Siratek: pitch and roll from gravity alone. Steady, but noisy when moved.

#include <Wire.h>
#include <MPU6050.h>
#include <math.h>

MPU6050 mpu;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  mpu.initialize();
}

void loop() {
  int16_t ax, ay, az;
  mpu.getAcceleration(&ax, &ay, &az);

  float roll  = atan2(ay, az) * 180.0 / PI;
  float pitch = atan2(-ax, sqrt((float) ay * ay + (float) az * az)) * 180.0 / PI;

  Serial.print("roll = ");
  Serial.print(roll, 1);
  Serial.print("   pitch = ");
  Serial.println(pitch, 1);

  delay(100);
}

Tilt it slowly and the angles follow nicely. Now tap the board: the numbers jump by tens of degrees, because the tap is an acceleration and the sensor cannot tell it from gravity.

That jumpiness is exactly what the gyro fixes.

The two combined

// Siratek: a complementary filter. Smooth like the gyro, stable like the accelerometer.

#include <Wire.h>
#include <MPU6050.h>
#include <math.h>

MPU6050 mpu;

float angle = 0;
unsigned long lastUpdate = 0;

// Measured at startup, with the board held still.
float gyroBias = 0;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  mpu.initialize();

  Serial.println("Siratek: hold it still for two seconds.");
  delay(500);

  long total = 0;
  for (int i = 0; i < 200; i++) {
    int16_t gx, gy, gz;
    mpu.getRotation(&gx, &gy, &gz);
    total += gx;
    delay(5);
  }
  gyroBias = total / 200.0;

  Serial.print("gyro bias = ");
  Serial.println(gyroBias, 1);

  // Start from the accelerometer, so the first reading is already right.
  int16_t ax, ay, az;
  mpu.getAcceleration(&ax, &ay, &az);
  angle = atan2(ay, az) * 180.0 / PI;

  lastUpdate = millis();
}

void loop() {
  int16_t ax, ay, az, gx, gy, gz;
  mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);

  unsigned long now = millis();
  float dt = (now - lastUpdate) / 1000.0;
  lastUpdate = now;

  float gyroRate = (gx - gyroBias) / 131.0;              // degrees per second
  float accelAngle = atan2(ay, az) * 180.0 / PI;

  angle = 0.98 * (angle + gyroRate * dt) + 0.02 * accelAngle;

  Serial.print("angle = ");
  Serial.println(angle, 1);

  delay(10);
}

Three details carry the weight.

The gyro bias is measured, not assumed. Every chip has a small constant offset, and without removing it the angle drifts steadily even when nothing moves.

dt is measured, not assumed. Using a fixed 0.01 because there is a delay(10) is wrong: the loop also spends time reading the sensor and printing. Timing it is one line and removes a whole class of error.

The 0.98 is adjustable. Higher trusts the gyro more, giving a smoother but slower-correcting angle. Lower lets the accelerometer in, which is steadier at rest and jumpier when moved. Start here and change it only after watching the behaviour you actually get.

Run this and tap the board: the angle barely twitches, where the accelerometer-only version jumped wildly. Then rotate it and bring it back: it returns to where it started instead of drifting away. That comparison is the point of the whole tutorial.

Expected result

Flat, az near 16384 and the others near zero. The filtered angle should track tilt smoothly, ignore taps, and return to its starting value when the board is put back flat.

Leave it still for several minutes and check the angle has not wandered. If it has, the gyro bias measurement was taken while the board was moving.

Troubleshooting

Symptom Likely cause Check or fix
testConnection() fails Wiring, or an address clash Scan the bus
Works alone, fails with a DS3231 Both at 0x68 Tie ADO to 3.3 V
All values zero initialize() not called Add it to setup()
az is not near 16384 when flat Different axis is up, or range changed Check which axis, and the full-scale setting
Angle drifts steadily Gyro bias not removed Measure it at startup, held still
Angle still drifts after that Board moved during calibration Hold it still and restart
Angle jumps when tapped Accelerometer only Use the complementary filter
Angle laggy Filter constant too high Lower it from 0.98
Values in counts, not degrees Raw output Divide by the scale in the tables
Readings noisy near a motor Vibration is real acceleration Mount it away from the motor

Next steps

  • Easier first: Detecting Movement with a Tilt Sensor, which answers yes or no rather than how far.
  • Related products: an SG90 servo for a self-levelling platform, or an OLED to show the angle.
  • Next project: a self-levelling camera mount, which is this filter driving two servos.

Sources and attribution

All text, wiring guidance and code in this tutorial are original Siratek work.

  • MPU-6050 datasheet for the I2C addresses, the ADO pin, and the full-scale ranges and sensitivity figures in both tables.
  • The GY-521 marking, the eight-pin header and the onboard regulator and power LED were read from the Siratek product photographs for item 2929.

The complementary filter is standard practice in inertial sensing rather than anything particular to this sensor, and the 0.98 constant is given as a starting point to adjust. No third-party tutorial text, diagrams, photographs or code has been reproduced here.

Product used in this tutorial: GY-521 MPU-6050 3 Axis Analog Gyroscope Sensors
Shop
Sidebar
0 Wishlist
0 Cart