Preview build — page text is a draft awaiting human review. Code badges come from real compiles. The flasher is real but not yet hardware-tested; the Playground is still a preview.
Seismograph: the finished build
UnoStudent projects

Classroom seismograph with a scrolling OLED trace (Arduino Uno + MPU6050)

Thump the table and watch the shaking scroll across the screen like a real seismogram.

Difficulty
Easy
Build time
1.5 h
Parts cost
₹935 · $11
Verified — core avr 1.8.8, Adafruit SSD1306 2.5.17, 3 Nov 2026Compile-only

Community draft — report a mistake

Parts list

PartQtyApprox.Buy
Arduino Uno R3 (CH340 clone)1₹450Buy Arduino Uno R3 (CH340 clone)
MPU6050 module (GY-521)1₹150Buy MPU6050 module (GY-521)
0.96" SSD1306 OLED, I2C, 4-pin1₹180Buy 0.96" SSD1306 OLED, I2C, 4-pin
Active buzzer module1₹25Buy Active buzzer module
Jumper wires + small breadboard1₹80Buy Jumper wires + small breadboard
Heavy base (tile or wooden block) and double-sided tape1₹50Buy Heavy base (tile or wooden block) and double-sided tape
Total₹935

Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.

Wiring

Arduino Uno R3Only the pins used are shown · 5 V logicMPU6050 (GY-521)SSD1306 OLED 0.96″ (128×64, I2C)Active buzzer module (high-level trigger)5VGNDA4A5D8SDA · 18SCL · 19VCCGNDSCLSDAXDA (not connected)XCL (not connected)AD0 (not connected)INT (not connected)GNDVCCSCLSDAI/OVCCGND
Every connection
Arduino Uno R3Part · pinWireNotes
A4MPU6050 (GY-521) · SDAblue
A5MPU6050 (GY-521) · SCLyellow
5VMPU6050 (GY-521) · VCCredThe GY-521 has its own 3.3 V regulator and 3.3 V pull-ups
GNDMPU6050 (GY-521) · GNDblackAD0 left open = address 0x68
SDASSD1306 OLED 0.96″ (128×64, I2C) · SDAblueThe SDA header pin is the same signal as A4
SCLSSD1306 OLED 0.96″ (128×64, I2C) · SCLyellowThe SCL header pin is the same signal as A5
5VSSD1306 OLED 0.96″ (128×64, I2C) · VCCred
GNDSSD1306 OLED 0.96″ (128×64, I2C) · GNDblack
D8Active buzzer module (high-level trigger) · I/Oorange
5VActive buzzer module (high-level trigger) · VCCred
GNDActive buzzer module (high-level trigger) · GNDblack

Board quirks: This is a teaching model. A real seismometer measures ground motion thousands of times smaller than a table thump, and India’s official earthquake information comes from the National Center for Seismology. Never present it as a warning system. Tape the MPU6050 flat and firmly to a heavy base. A sensor hanging on its wires records the wires swinging, not the table. Keep it still for the first half second after power-up: the sketch learns the resting 1 g of gravity then and subtracts it from every reading. MPU6050 modules are cheap clones; the noise floor is around 5–10 mg. Footsteps on a wooden stage show up; a person walking past a concrete floor usually doesn’t.

Code

mpu6050-seismograph.inoArduino C++Open in PlaygroundOpen in PlaygroundFlash
// Classroom seismograph — Arduino Uno + MPU6050 (GY-521) + 0.96" SSD1306 OLED + active buzzer module.
// Samples the accelerometer 100 times a second, removes the steady 1 g of gravity, and draws the
// shaking as a scrolling trace (one column every 50 ms, newest on the right). Above 0.15 g the
// buzzer sounds. A teaching model of a seismometer — NOT an earthquake warning system.
// MPU6050 and OLED share I2C: SDA → A4, SCL → A5 (MPU6050 0x68, OLED 0x3C). Buzzer I/O → D8.
// Serial (115200): one "time_ms,shake_mg" line per column — open the Serial Plotter.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

constexpr uint8_t MPU_ADDR = 0x68;
constexpr uint8_t BUZZER_PIN = 8;
constexpr int ALARM_MG = 150;         // shaking (above gravity) that sounds the buzzer
constexpr int FULL_SCALE_MG = 200;    // trace height = this many mg (bigger shakes are clipped)
constexpr uint8_t TRACE_TOP = 16, TRACE_H = 48;

Adafruit_SSD1306 oled(128, 64, &Wire, -1);
int8_t trace[128];                    // one column per 50 ms, in pixels above/below the centre
uint8_t head = 0;
bool mpuOk = false;
long baseline = 1000;                 // |a| at rest in mg, learned while still
int peakMg = 0, windowPeak = 0;
unsigned long lastSample = 0, lastColumn = 0, lastRetry = 0, buzzOff = 0;

bool mpuWrite(uint8_t reg, uint8_t val) {
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(reg);
  Wire.write(val);
  return Wire.endTransmission() == 0;
}

// Magnitude of acceleration in mg (±2 g range: 16384 counts per g), or -1 if the read fails.
long readMagnitudeMg() {
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x3B);  // ACCEL_XOUT_H
  if (Wire.endTransmission(false) != 0) return -1;
  if (Wire.requestFrom(MPU_ADDR, (uint8_t)6) != 6) return -1;
  float sum = 0;
  for (uint8_t i = 0; i < 3; i++) {
    uint8_t hi = Wire.read();  // separate statement: read order inside one expression isn't defined
    int16_t raw = (hi << 8) | Wire.read();
    float g = raw / 16384.0;
    sum += g * g;
  }
  return lround(sqrt(sum) * 1000);
}

bool mpuBegin() {
  if (!mpuWrite(0x6B, 0x00)) return false;  // PWR_MGMT_1: wake up, internal clock
  mpuWrite(0x1C, 0x00);                     // ACCEL_CONFIG: ±2 g, the most sensitive range
  mpuWrite(0x1A, 0x03);                     // CONFIG: 44 Hz low-pass filter
  delay(50);
  // Learn the resting value of |a| (≈1000 mg) over half a second. Keep the sensor still.
  long sum = 0;
  uint8_t n = 0;
  for (uint8_t i = 0; i < 50; i++) {
    long m = readMagnitudeMg();
    if (m > 0) sum += m, n++;
    delay(10);
  }
  if (!n) return false;
  baseline = sum / n;
  return true;
}

void drawScreen(int nowMg) {
  oled.clearDisplay();
  oled.setTextSize(1);
  oled.setTextColor(SSD1306_WHITE);
  oled.setCursor(0, 0);
  if (!mpuOk) {
    oled.println(F("Seismograph"));
    oled.println(F("MPU6050 missing!"));
    oled.println(F("Check SDA A4, SCL A5"));
    oled.println(F("and VCC/GND."));
    oled.display();
    return;
  }
  oled.print(F("Now "));
  oled.print(nowMg);
  oled.print(F("mg  Peak "));
  oled.print(peakMg);
  oled.setCursor(0, 8);
  oled.print(peakMg >= ALARM_MG ? F("STRONG SHAKING") : peakMg >= 30 ? F("Felt") : F("Quiet"));
  int mid = TRACE_TOP + TRACE_H / 2;
  for (uint8_t x = 0; x < 128; x += 8) oled.drawPixel(x, mid, SSD1306_WHITE);  // dotted zero line
  for (uint8_t x = 0; x < 128; x++) {
    int8_t v = trace[(head + x) % 128];  // oldest on the left
    oled.drawFastVLine(x, v >= 0 ? mid - v : mid, abs(v) + 1, SSD1306_WHITE);
  }
  oled.display();
}

void setup() {
  Serial.begin(115200);
  pinMode(BUZZER_PIN, OUTPUT);
  Wire.begin();
  if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) Serial.println(F("OLED not found at 0x3C"));
  oled.clearDisplay();
  oled.setTextColor(SSD1306_WHITE);
  oled.setCursor(0, 0);
  oled.println(F("Seismograph"));
  oled.println(F("Keep still..."));
  oled.display();
  mpuOk = mpuBegin();
  Serial.println(mpuOk ? F("MPU6050 ready. Baseline (mg):") : F("MPU6050 not found - check SDA A4, SCL A5, VCC, GND"));
  if (mpuOk) Serial.println(baseline);
  Serial.println(F("time_ms,shake_mg"));
  drawScreen(0);
}

void loop() {
  unsigned long now = millis();
  if (buzzOff && now > buzzOff) {
    digitalWrite(BUZZER_PIN, LOW);
    buzzOff = 0;
  }
  if (!mpuOk) {
    if (now - lastRetry > 2000) {  // keep trying, so a loose wire can be fixed live
      lastRetry = now;
      mpuOk = mpuBegin();
      if (mpuOk) Serial.println(F("MPU6050 found"));
      drawScreen(0);
    }
    return;
  }

  if (now - lastSample >= 10) {  // 100 samples per second
    lastSample = now;
    long m = readMagnitudeMg();
    if (m < 0) {
      mpuOk = false;
      Serial.println(F("MPU6050 stopped answering"));
      return;
    }
    int dev = m - baseline;
    // Let the baseline follow very slow drift (temperature), not the shaking itself.
    baseline += (m - baseline) / 256;
    if (abs(dev) > abs(windowPeak)) windowPeak = dev;
  }

  if (now - lastColumn >= 50) {  // one trace column per 50 ms → 6.4 s across the screen
    lastColumn = now;
    int mg = windowPeak;
    windowPeak = 0;
    int px = constrain((long)mg * (TRACE_H / 2) / FULL_SCALE_MG, -(TRACE_H / 2 - 1), TRACE_H / 2 - 1);
    trace[head] = px;
    head = (head + 1) % 128;
    // Peak of what is on screen now.
    peakMg = 0;
    for (uint8_t i = 0; i < 128; i++) peakMg = max(peakMg, abs(trace[i]) * FULL_SCALE_MG / (TRACE_H / 2));
    peakMg = max(peakMg, abs(mg));
    if (abs(mg) >= ALARM_MG) {
      digitalWrite(BUZZER_PIN, HIGH);
      buzzOff = now + 1000;
    }
    Serial.print(now);
    Serial.print(',');
    Serial.println(mg);
    drawScreen(abs(mg));
  }
}

Compiled for uno on avr 1.8.8 with Adafruit SSD1306 2.5.17 · 3 Nov 2026

How it works

A seismograph turns invisible shaking into a picture. This one uses the accelerometer inside an MPU6050 — the same kind of sensor that rotates your phone screen — and draws a scrolling trace on a small OLED, just like the paper drum in a textbook diagram.

How it works

The MPU6050 measures acceleration on three axes. At rest it still reads about 1 g (1000 mg), because gravity pulls on its tiny test mass. The sketch combines the three axes into one magnitude, √(x² + y² + z²), so it doesn’t matter which way the module is mounted, then subtracts the resting value it learned at power-up. What’s left is the shaking.

The Uno reads the sensor over I2C 100 times a second with plain Wire calls (no library — it saves memory for the OLED’s 1 KB screen buffer). Every 50 ms the biggest reading of that window becomes one column of the trace, so the 128-pixel screen shows the last 6.4 seconds. The top line shows the current shaking and the peak on screen in mg; the second line says Quiet, Felt or STRONG SHAKING. Above 150 mg the buzzer sounds for a second.

The Serial Monitor prints time_ms,shake_mg for every column — open Tools → Serial Plotter and the laptop draws a big version of the same seismogram.

What to show at the exhibition

  • Tap the table softly, then harder: the spikes grow. Ask visitors to guess which tap will set off the buzzer.
  • Drop a coin from 10 cm and from 30 cm at the same distance: a harder impact gives a bigger spike.
  • Tap close to the sensor and at the far end of the table: the shaking dies away with distance, which is why earthquakes are felt most near the epicentre.
  • Put a chart beside it explaining P-waves (arrive first, push–pull) and S-waves (arrive later, side to side) and the Richter/moment-magnitude idea.

Questions the examiner may ask

  • Why does a still sensor read 1 g? An accelerometer measures the force on its test mass; gravity pulls on it all the time, so at rest it reads 1 g upwards.
  • What is mg? One-thousandth of g (9.81 m/s²), so 150 mg ≈ 1.5 m/s².
  • How is this different from a real seismometer? Real ones detect ground movement of nanometres, are buried on bedrock away from traffic, and record at precise times from GPS so several stations can locate the epicentre.
  • Why 100 samples per second? Shaking of a table or a building is mostly below 20 Hz; sampling well above twice that frequency (the Nyquist rule) captures it.
  • What does the low-pass filter do? The MPU6050’s built-in 44 Hz filter removes fast electrical noise before the Uno reads the value.

Cost in India

About ₹950: the GY-521 module is ₹120–180 and the OLED ₹150–220 at local shops or online. No extra resistors are needed — the GY-521 has its own regulator and pull-ups.

Troubleshooting

The OLED says “MPU6050 missing!”

No reply at 0x68. Check SDA on A4 and SCL on A5 (not swapped), VCC and GND. If AD0 is tied to 3.3 V the address is 0x69 — leave AD0 open. The sketch retries every 2 s, so it recovers as soon as the wire is fixed.

The trace is never flat

Something is vibrating — a fan on the same table, or the board was moved after power-up. Press the Uno’s reset with everything still to learn a new baseline.

The buzzer goes off at every small tap

Raise ALARM_MG from 150 to 300 for a demo on a light table.

Blank OLED

Some 0.96" OLEDs are at 0x3D. Change 0x3C in oled.begin(), or run an I2C scanner to see both addresses.

Upgrades

  • Log the trace to a microSD card with a timestamp from a DS3231 RTC.
  • Build a “shake table” from a cardboard box on pencils and compare model buildings with and without cross-bracing.
  • Send the Serial CSV to a laptop and compute the dominant frequency with a spreadsheet FFT.
  • Use three axes separately to tell side-to-side (S-wave-like) from up-down (P-wave-like) motion.

↑↓ to move · Enter to open