Digital compass and tilt display breadboard build with ESP32-C3 SuperMini, MPU6050 sensor, and OLED display showing heading

Beginner

Build a Digital Compass & Tilt Display

SensorsDisplays

Point it and watch a real heading track live on screen, real orientation sensing, the same class of sensor behind a phone's own compass and auto-rotate.

3 real parts included

Total for this kit: 1,170.00 ฿

Doesn't include any order discount you may get at checkout.

What you'll need

Project Goal

This project builds a real orientation display: tilt or rotate the sensor and watch the screen track it live, the same underlying sensing technology behind a phone's auto-rotate and every drone's flight stabilization, just made visible on a small screen instead of hidden inside a bigger device.

What the Controller Does

It continuously reads the sensor's raw motion values, converts them into a usable tilt or heading figure, and updates the display, dozens of times a second, so the readout feels genuinely live rather than laggy.

What the IMU Does

It combines an accelerometer, which senses the direction of gravity, with a gyroscope, which senses rotation speed, giving the controller everything it needs to work out orientation and motion in real time over a simple I2C interface.

What the Display Does

It shows the current tilt or heading reading, either as raw numbers or, with a bit more code, as a simple visual indicator like a horizon line or tilting bar.

Software and Library Concepts

Start by printing the raw accelerometer and gyroscope values to confirm the sensor is wired and communicating correctly, then work on combining them into a stable tilt angle before worrying about the display at all.

Testing and Troubleshooting

  • Readings drift slowly over time even when the sensor is perfectly still, this is a normal characteristic of gyroscopes, look into a simple complementary or Kalman filter combining both sensors rather than trusting the gyroscope alone.
  • The sensor reads nothing or a fixed constant value, double check the I2C wiring and address, and confirm no other I2C device on the same bus is using a conflicting address.
  • Values jump around at rest, the sensor may need a brief calibration step at startup, reading and averaging its resting values before using them as a baseline.

Optional Upgrade

Add the buzzer from the parts bin to sound an alert if the tilt angle crosses a threshold, a simple physical level-alarm built from the same sensor.

Wiring

  • MPU6050 VCC → ESP32-C3 3.3V
  • MPU6050 GND → ESP32-C3 GND
  • MPU6050 SDA → ESP32-C3 GPIO8 (default I2C)
  • MPU6050 SCL → ESP32-C3 GPIO9 (default I2C)
  • OLED VCC → ESP32-C3 3.3V
  • OLED GND → ESP32-C3 GND
  • OLED SDA → ESP32-C3 GPIO8 (shared bus with MPU6050)
  • OLED SCL → ESP32-C3 GPIO9 (shared bus with MPU6050)

Be honest with yourself about what this project is: the MPU6050 is an accelerometer + gyroscope only — it has no magnetometer, so it physically cannot sense magnetic north and cannot produce a real compass heading. What's below is a genuine tilt/level display using accelerometer pitch and roll, which is accurate at rest or slow motion. A true magnetic compass would need an added magnetometer (e.g. HMC5883L or QMC5883L), which isn't in this parts list.

Wiring Diagram

VCC → 3.3V GND SDA → GPIO8 SCL → GPIO9 VCC → 3.3V GND SDA → GPIO8 SCL → GPIO9 ESP32-C3 MPU6050 OLED

Starter Code

#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- ESP32-C3 SuperMini default I2C pins - adjust if your board differs ---
#define SDA_PIN 8
#define SCL_PIN 9

#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_ADDR     0x3C

Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);

  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("OLED init failed");
    while (true) delay(1000);
  }

  if (!mpu.begin()) {
    Serial.println("MPU6050 not found - check wiring/address");
    while (true) delay(1000);
  }
  mpu.setAccelerometerRange(MPU6050_RANGE_8_G);
  mpu.setGyroRange(MPU6050_RANGE_500_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
}

void loop() {
  sensors_event_t accel, gyro, temp;
  mpu.getEvent(&accel, &gyro, &temp);

  // Pitch/roll from the accelerometer's gravity vector - valid at rest / slow motion
  float pitch = atan2(accel.acceleration.y,
                       sqrt(accel.acceleration.x * accel.acceleration.x +
                            accel.acceleration.z * accel.acceleration.z)) * 180.0 / PI;
  float roll = atan2(-accel.acceleration.x, accel.acceleration.z) * 180.0 / PI;

  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Tilt Display");
  display.setCursor(0, 16);
  display.print("Pitch: ");
  display.print(pitch, 1);
  display.println((char)247);
  display.setCursor(0, 28);
  display.print("Roll:  ");
  display.print(roll, 1);
  display.println((char)247);

  // simple bubble-level dot
  int cx = 64, cy = 52;
  int dotX = constrain(cx + (int)roll, 20, 108);
  int dotY = constrain(cy + (int)pitch, 40, 63);
  display.drawCircle(cx, cy, 12, SSD1306_WHITE);
  display.fillCircle(dotX, dotY, 2, SSD1306_WHITE);

  display.display();
  delay(100);
}

Not sure about a step? Ask us