The MPU6050 and Arduino combination is the benchmark for hobbyist 6-axis motion tracking. The MPU-6050 communicates strictly via I2C, operates at a default hex address of 0x68, and outputs 16-bit raw ADC data for both its MEMS accelerometer and gyroscope. The code and wiring in this guide specifically target the Arduino Uno R3 (ATmega328P) paired with the ubiquitous GY-521 breakout board. We bypass heavy third-party libraries to read raw registers directly via the Wire.h library, giving you total control over the I2C bus and eliminating dependency conflicts.

MPU6050 and Arduino Hardware Specs & Pin Mapping

Before stripping wires, you need to understand the electrical boundaries of the GY-521 breakout. Unlike bare MPU-6050 silicon which requires a strict 3.3V supply, the GY-521 module includes an onboard low-dropout (LDO) regulator. This means you must supply 5V to the VCC pin to feed the LDO, while the I2C logic lines (SDA/SCL) are pulled up to 3.3V internally.

Table 1: MPU-6050 Sensor Specifications & Operating Ranges
Parameter Value / Range Register / Notes
Accelerometer Range ±2g, ±4g, ±8g, ±16g Configured via AFS_SEL (Reg 0x1C)
Gyroscope Range ±250, ±500, ±1000, ±2000 °/s Configured via FS_SEL (Reg 0x1B)
ADC Resolution 16-bit (Two's Complement) Output range: -32768 to 32767
I2C Clock Speed 400 kHz (Fast Mode) Standard 100kHz also supported
Operating Voltage (GY-521) 4.5V to 5.5V at VCC pin Onboard LDO regulates to 3.3V
Quiescent Current 3.9 mA Gyro + Accel active, no DMP

Exact Pin Mapping for Arduino Uno R3

Table 2: GY-521 to Arduino Uno R3 Wiring
GY-521 Pin Arduino Uno R3 Pin Function & Wiring Notes
VCC 5V Main power input for the breakout LDO.
GND GND Common ground. Must share ground with Uno.
SCL A5 I2C Clock line. Hardware I2C on ATmega328P.
SDA A4 I2C Data line. Hardware I2C on ATmega328P.
XDA Not Connected Auxiliary I2C master. Leave floating.
AD0 GND I2C Address Select. LOW = 0x68, HIGH = 0x69.
INT Pin 2 Interrupt pin. Optional for basic polling reads.

Step-by-Step Wiring and I2C Address Verification

Follow this sequence to avoid the most common I2C bus lockups. The ATmega328P hardware I2C peripheral is unforgiving if lines are swapped or shorted.

  1. Establish Common Ground: Connect the GND pin on the GY-521 to any GND pin on the Arduino Uno. Do this before applying power to prevent floating ground potentials from damaging the MEMS silicon.
  2. Power the Breakout: Connect the GY-521 VCC to the Arduino 5V pin. Do not use the 3.3V pin. The 3.3V output on the Uno R3 is sourced from the USB-serial bridge and cannot reliably supply the inrush current of the MPU6050 during startup.
  3. Wire the I2C Bus: Connect SDA to A4 and SCL to A5. Double-check this. Swapping them will not physically damage the board, but it will cause the I2C state machine to hang indefinitely.
  4. Hardwire the Address: Connect the AD0 pin directly to GND. While the internal pull-down resistor usually defaults the address to 0x68, stray flux or breadboard leakage can float this pin, shifting the address to 0x69 and causing silent connection failures.
Callout Tip: Pull-up Resistors
The GY-521 breakout includes 4.7kΩ pull-up resistors on the SDA and SCL lines tied to 3.3V. If you are chaining multiple I2C devices on the same bus, the parallel resistance will drop, potentially violating the I2C rise-time specification. If your bus capacitance exceeds 200pF, remove the surface-mount pull-ups on the GY-521 and use a dedicated 3.3V I2C level-shifter with active pull-ups.

Complete Arduino Code: Reading Raw Accelerometer and Gyroscope Data

This sketch uses only the native Wire.h library. It wakes the sensor, configures the ranges, and reads the 14-byte data burst (Accel XYZ, Temp, Gyro XYZ). It includes explicit I2C error handling to catch bus lockups before they freeze the microcontroller.

#include <Wire.h>

// Hardware I2C pins for Arduino Uno R3
const int PIN_SDA = A4;
const int PIN_SCL = A5;
const int PIN_INT = 2; // Data ready interrupt (optional)

const int MPU_ADDR = 0x68; // AD0 pin tied to GND

// Scale factors based on configured ranges
const float ACCEL_SCALE = 16384.0; // ±2g range (LSB/g)
const float GYRO_SCALE = 131.0;    // ±250°/s range (LSB/°/s)

void setup() {
  Serial.begin(115200);
  Wire.begin(); // Init I2C bus on hardware pins A4/A5

  // 1. Wake up the MPU6050 (it starts in sleep mode)
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x6B); // PWR_MGMT_1 register
  Wire.write(0);    // Write 0 to wake up
  byte error = Wire.endTransmission();
  
  if (error != 0) {
    Serial.print("MPU6050 I2C init failed. Wire.endTransmission() returned error code ");
    Serial.println(error);
    while(1); // Halt execution to prevent I2C bus spam
  }

  // 2. Configure Accelerometer to ±2g
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x1C); // ACCEL_CONFIG register
  Wire.write(0x00); // AFS_SEL = 0 (±2g)
  Wire.endTransmission();

  // 3. Configure Gyroscope to ±250°/s
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x1B); // GYRO_CONFIG register
  Wire.write(0x00); // FS_SEL = 0 (±250°/s)
  Wire.endTransmission();

  Serial.println("MPU6050 initialized successfully.");
}

void loop() {
  // Request 14 bytes starting from ACCEL_XOUT_H (0x3B)
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x3B);
  byte txError = Wire.endTransmission(false); // Repeated start condition
  
  if (txError != 0) {
    Serial.print("I2C TX Error: ");
    Serial.println(txError);
    delay(1000);
    return;
  }

  byte bytesReceived = Wire.requestFrom(MPU_ADDR, 14, true);
  if (bytesReceived != 14) {
    Serial.print("I2C RX Error: Expected 14 bytes, got ");
    Serial.println(bytesReceived);
    delay(1000);
    return;
  }

  // Parse 16-bit two's complement values
  int16_t AcX = (Wire.read() << 8) | Wire.read();
  int16_t AcY = (Wire.read() << 8) | Wire.read();
  int16_t AcZ = (Wire.read() << 8) | Wire.read();
  int16_t Tmp = (Wire.read() << 8) | Wire.read();
  int16_t GyX = (Wire.read() << 8) | Wire.read();
  int16_t GyY = (Wire.read() << 8) | Wire.read();
  int16_t GyZ = (Wire.read() << 8) | Wire.read();

  // Convert raw ADC to physical units
  float accX_g = AcX / ACCEL_SCALE;
  float accY_g = AcY / ACCEL_SCALE;
  float accZ_g = AcZ / ACCEL_SCALE;
  
  float gyroX_dps = GyX / GYRO_SCALE;
  float gyroY_dps = GyY / GYRO_SCALE;
  float gyroZ_dps = GyZ / GYRO_SCALE;
  
  float temp_c = (Tmp / 340.0) + 36.53;

  // Output to Serial Plotter / Monitor
  Serial.print("Accel(g): "); Serial.print(accX_g); Serial.print(", "); Serial.print(accY_g); Serial.print(", "); Serial.print(accZ_g);
  Serial.print(" | Gyro(dps): "); Serial.print(gyroX_dps); Serial.print(", "); Serial.print(gyroY_dps); Serial.print(", "); Serial.print(gyroZ_dps);
  Serial.print(" | Temp(C): "); Serial.println(temp_c);

  delay(100); // 10Hz read rate
}

Debugging: 'No I2C Devices Found' and Sensor Drift

When working with the MPU6050 and Arduino, I2C bus failures are the primary roadblock. If you run an I2C Scanner sketch and see the exact error string No I2C devices found, or if the code above halts with Wire.endTransmission() returned error code 2, the microcontroller is failing to receive an ACKnowledge (ACK) bit from the sensor.

The First Three Things to Check

  1. Power and Logic Levels (Error Code 2 or 4): Verify you are feeding 5V to the VCC pin, not 3.3V. If the onboard LDO is starved, the MEMS sensor will brownout during the I2C handshake. Measure the voltage at the VCC pin with a multimeter; it must read >4.7V.
  2. SDA/SCL Swap (Silent Lockup): The ATmega328P will not throw an error if SDA and SCL are reversed; it will simply hang at Wire.requestFrom() because the clock line is waiting for a data edge. Verify A4 is SDA and A5 is SCL.
  3. AD0 Pin State (Address Shift): If AD0 is accidentally pulled high, the sensor responds to 0x69. Run Nick Gammon's I2C Scanner or the standard Arduino scanner to check if a device appears at 0x69 instead of 0x68. Explicitly wire AD0 to GND to eliminate this variable.

Decoding Wire.endTransmission() Errors

According to the Arduino Wire Library Reference, the return byte from endTransmission() tells you exactly where the bus failed:

  • 0: Success. The MPU6050 acknowledged the address and data.
  • 1: Data too long to fit in transmit buffer. (Rare on Uno, indicates a code logic error).
  • 2: Received NACK on transmit of address. The sensor is not at 0x68, is unpowered, or SDA/SCL are swapped.
  • 3: Received NACK on transmit of data. The sensor is awake but rejected the register address you tried to write to.
  • 4: Other error. Usually a physical bus short or missing common ground.

Extending the Build: Sensor Fusion and Calibration

Raw accelerometer data is noisy, and raw gyroscope data suffers from integration drift over time. To build a functional digital spirit level or robotic balancer, you must process the raw data.

How to Simplify: Single-Axis Tilt

If you only need a simple tilt alarm (e.g., a tamper switch for a project box), strip the code down. Read only ACCEL_ZOUT_H and ACCEL_ZOUT_L. If the Z-axis value drops below 0.8g (meaning the board is tilted more than ~36 degrees off horizontal), trigger a digital output pin. This requires no sensor fusion and runs flawlessly at 1Hz.

How to Extend: Madgwick AHRS Filter

For full 3D orientation (Yaw, Pitch, Roll), you need to fuse the accelerometer's absolute gravity reference with the gyroscope's high-frequency rotational data. The Madgwick AHRS filter is the industry standard for this. It uses a gradient descent algorithm to minimize the error between the estimated gravity vector and the measured gravity vector.

While the MPU-6050 features an onboard Digital Motion Processor (DMP) capable of doing this math in hardware, reverse-engineering the proprietary DMP firmware blob is brittle and consumes significant ATmega328P flash memory. Running a software-based Madgwick or Mahony filter on the Arduino Uno is highly recommended for modern builds. You will need to sample the sensor at a strict 100Hz or 200Hz interval using hardware interrupts on Pin 2 (the INT pin) rather than using delay() in the main loop to ensure the filter's delta-time (dt) variable remains mathematically stable.

For deep-dive register maps and electrical characteristics, always refer to the official TDK InvenSense MPU-6050 Product Specification. Remember that while the GY-521 breakout handles the 5V-to-3.3V power regulation, the I2C data lines are still 3.3V logic; if you migrate this build to a 5V-tolerant board like the Arduino Mega, the I2C pins remain safe, but moving to a strictly 3.3V board like the ESP32 requires wiring the GY-521 VCC to 3.3V and bypassing the onboard LDO.