Tilt and Motion with an MPU-6050
| Difficulty | Advanced |
|---|---|
| Category | Sensors |
| Supported boards | Arduino Uno R3 and compatible ATmega328P boards |
| Time needed | 65 minutes |
| Prerequisites | I2C, Two Wires and Many Devices |
| Tested board | Not yet verified |
| Tested on | Not yet verified |
| Arduino IDE version | Not yet verified |
| Library | Not 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 |
Wiring steps
- Unplug the USB cable.
GNDtoGND,VCCto5V.SDAtoA4,SCLtoA5.- If a DS3231 is also on the bus, run a wire from
ADOto3.3V. - Plug the USB cable back in and scan the bus. Expect 0x68, or 0x69 if you tied
ADOhigh.
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
ADOpin, and the full-scale ranges and sensitivity figures in both tables. - The
GY-521marking, 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.

