Blog · Tutorial

MPU-6050 tutorial: motion sensing over i2c

Tutorial

Six axes of motion for about a dollar. The MPU-6050 is the sensor behind a thousand self-balancing robots — and it ships asleep, sits on the busiest address on the bus, and its gyro drifts by design. All three are fine once you know.

What it measures

Accelerometer and gyroscope: two different questions

The MPU-6050 is two sensors sharing a package, and confusing them is where most projects go wrong.

The accelerometer measures acceleration in g, on three axes. Sitting still on your desk it reads ~1 g on Z and ~0 on X and Y — because gravity is an acceleration, and the sensor can't distinguish it from any other. That's not a flaw; it's what makes the accelerometer useful for tilt. Which way is down is a question it answers absolutely and forever, without drift.

The gyroscope measures rate of rotation in degrees per second. Not angle — rate. Still on the desk it reads ~0 on all three axes no matter how it's oriented, and spinning it reads how fast, not how far.

They're complementary in the literal sense: the accelerometer is stable but noisy and useless while the thing is accelerating; the gyro is smooth and fast but only tells you about change. Nearly every real application fuses both — which is the last section of this article.

Step 1

MPU-6050 wiring and the 0x68 problem

Four wires plus one that matters: VCC (3.3–5 V on a regulated breakout — the bare chip is 3.3 V, but the common GY-521 board has a regulator), GND, SDA, SCL, and AD0.

AD0 picks the address: low gives 0x68, high gives 0x69. Most breakouts pull it low, so 0x68 is the default you'll meet. INT is an optional interrupt output.

And here's the thing to sort out before you solder: 0x68 is the most contested address on i2c. The DS3231 real-time clock is fixed there — no pins, no jumper, no escape. The AMG8833 thermal camera defaults to 0x69, the MPU's own alternate.

A datalogger that records when something moved is the most natural project in the world, and it puts an MPU-6050 and a DS3231 on the same two wires. Since the clock can't move, the MPU does: tie AD0 high, go to 0x69, done. That single wire is the whole fix. If you also want a Grid-EYE — which wants 0x69 — then you're out of straps and into multiplexer territory. The collision roundup maps the whole neighbourhood.

Scan and confirm 0x68 before writing code.

Step 2

Why the MPU-6050 reads all zeros: it's asleep

This is the number one MPU-6050 question on the internet, and the answer is one bit.

The MPU-6050 powers up in sleep mode. Deliberately — it's a power-saving default. The SLEEP bit in register PWR_MGMT_1 (0x6B) is set at reset, and while it is, every measurement register reads zero. The chip acknowledges its address perfectly, so a scan finds it, and reads return clean, plausible zeros. Nothing looks broken. Nothing is broken.

Clear that bit and it wakes:

# Raspberry Pi — prove it with no code at all.
# read PWR_MGMT_1: 0x40 means the SLEEP bit is set
i2cget -y 1 0x68 0x6B

# clear it — wake the chip
i2cset -y 1 0x68 0x6B 0x00

# now read the accelerometer X high byte — no longer zero if it's awake
i2cget -y 1 0x68 0x3B

Every real library does this in begin(), which is why most people never learn it. It only bites when you write raw register code — and then it bites hard, because there's no error to chase.

Step 3 · Arduino

MPU-6050 Arduino code: read motion

Install Adafruit's MPU6050 library, then:

#include <Adafruit_MPU6050.h>

Adafruit_MPU6050 mpu;

void setup() {
  Serial.begin(115200);
  if (!mpu.begin(0x68)) {              // 0x69 if you tied AD0 high
    Serial.println("No MPU-6050 at 0x68 — check wiring and AD0");
    while (1);
  }
  // begin() clears the SLEEP bit for you. Ranges: smallest that fits.
  mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
  mpu.setGyroRange(MPU6050_RANGE_500_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);   // tames the noise
}

void loop() {
  sensors_event_t a, g, temp;
  mpu.getEvent(&a, &g, &temp);

  Serial.printf("accel %6.2f %6.2f %6.2f m/s^2   gyro %6.2f %6.2f %6.2f rad/s\n",
    a.acceleration.x, a.acceleration.y, a.acceleration.z,
    g.gyro.x, g.gyro.y, g.gyro.z);
  delay(100);
}

Flat on the desk you should see ~9.8 on the Z axis — that's gravity in m/s², and it's the quickest sanity check there is. If Z reads 9.8 and the others hover near zero, everything works.

The ranges trade span for resolution, exactly like the ADS1115's gain. Output is always 16 bits, so a smaller range spreads them over less: at ±2 g a count is about 1/16384 g; at ±16 g only 1/2048. Pick the smallest range your motion fits inside. A tilt sensor wants ±2 g; something that gets thrown wants ±16 g.

The filter bandwidth is worth setting too. MEMS sensors are electrically noisy, and the built-in low-pass filter costs nothing — 21 Hz is a reasonable default for anything human-scale.

Step 4

MPU-6050 registers and scaling raw counts

Worth seeing once, because the scaling explains what the library returns:

RegisterHolds
0x3B–0x40accel X, Y, Z — 6 bytes, high byte first
0x41–0x42temperature
0x43–0x48gyro X, Y, Z — 6 bytes, high byte first
0x6BPWR_MGMT_1 — the SLEEP bit lives here
0x75WHO_AM_I — 0x68 = MPU-6050, 0x70 = 6500, 0x71 = 9250

Every value is a signed 16-bit number split across two registers, high byte first. Raw counts mean nothing until you divide by the sensitivity for your chosen range:

accel_g     = raw / 16384.0    // at +/-2 g
accel_g     = raw /  2048.0    // at +/-16 g
gyro_dps    = raw /   131.0    // at +/-250 deg/s
gyro_dps    = raw /    16.4    // at +/-2000 deg/s

Change the range and forget the divisor and your readings are wrong by a clean factor of 2, 4 or 8 — which looks like a calibration problem and isn't.

That WHO_AM_I at 0x75 is the honest way to know what you bought. Modules labelled MPU-6050 sometimes ship a 6500; the register doesn't lie. Our i2c tool reads it automatically and names the chip.

The hard part

Why your gyro angle drifts, and how to fix it

You want an angle. The gyro gives a rate. So you integrate — add up rate × time, over and over.

And integration is unforgiving: every tiny bias in the gyro's zero point gets added in, every single sample, forever. A bias of 0.1 deg/s — well within spec, undetectable by eye — becomes 6 degrees after a minute and 360 degrees after an hour. Your perfectly still sensor slowly rotates through a full circle. Nothing is broken. This is what gyros do.

The accelerometer has the opposite problem. It knows which way gravity points, so its tilt estimate never drifts — but it's noisy, and any real acceleration (the robot starting to move) corrupts it completely, because it can't tell gravity from motion.

So: fuse them. Trust the gyro over the short term and the accelerometer over the long term. The cheapest version is a complementary filter, and it genuinely is this simple:

// gyro for fast changes, accel to stop the drift
angle = 0.98 * (angle + gyro_dps * dt) + 0.02 * accel_angle;

98% of the new angle comes from integrating the gyro — smooth and responsive. 2% comes from the accelerometer, and that trickle is enough to continuously pull the estimate back to reality before the drift accumulates. A Kalman filter does the same job with more maths and better results; for a balancing robot, the one-liner above is usually enough.

Want the heading too? An accelerometer and gyro can't give you absolute yaw — gravity says nothing about which way is north. That needs a magnetometer, which is what the MPU-9250 adds, or a dedicated 9-axis part like the BNO080 that does the fusion on-chip and hands you a ready-made orientation.

Get one

Questions

MPU-6050 FAQ

What is the MPU-6050's i2c address?

0x68 with the AD0 pin low, 0x69 with it high — and most breakouts pull AD0 low for you, so 0x68 is what you'll see. That's worth knowing before you wire anything, because 0x68 is also the DS3231 real-time clock's fixed address. The clock can't move; the MPU can. Tie AD0 high and the conflict disappears.

Why does my MPU-6050 read all zeros?

Because it's asleep. The MPU-6050 powers up in sleep mode by design — the SLEEP bit in the PWR_MGMT_1 register (0x6B) is set at reset, and until you clear it every measurement register reads zero. It's the single most common MPU-6050 problem, and any real library clears it in begin(). If you're writing raw register code, that write is step one.

What's the difference between the MPU-6050, MPU-6500 and MPU-9250?

The 6050 is the original 6-axis part (3-axis accelerometer + 3-axis gyro). The MPU-6500 is its newer, lower-power 6-axis replacement. The MPU-9250 is a 6500 with a magnetometer added, making it 9-axis, which is what you need for absolute heading rather than just relative rotation. They share the 0x68/0x69 addresses but have different WHO_AM_I values — 0x68, 0x70 and 0x71 respectively — which is the reliable way to tell what you actually received.

Why does my gyro angle drift even when the sensor is still?

Because a gyroscope measures rate of rotation, not angle. To get an angle you integrate that rate over time, and integration accumulates every tiny bias error forever — so a stationary sensor's angle creeps steadily away. This isn't a fault, it's what gyros do. The fix is to fuse the gyro with the accelerometer, which knows which way gravity points and doesn't drift, using a complementary or Kalman filter.

What are the accelerometer and gyro range settings for?

They trade range against resolution, the same way an ADC's gain does. The accelerometer offers ±2, ±4, ±8 or ±16 g; the gyro ±250, ±500, ±1000 or ±2000 deg/s. The output is always a 16-bit number, so a smaller range spreads those bits over less — at ±2 g you get about 16384 counts per g, at ±16 g only 2048. Pick the smallest range your motion actually fits inside.

Is the MPU-6050 obsolete?

It's discontinued by InvenSense but remains everywhere and dirt cheap, and for learning or a hobby project it's perfectly good. If you're designing something to still be buildable in five years, look at the LSM6DS3 or ICM-42688. For a weekend build, the MPU-6050's enormous body of tutorials and library support is a genuine advantage.