Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| Arduino Uno R3 (CH340 clone) | 1 | ₹450 | Buy Arduino Uno R3 (CH340 clone) |
| MPU6050 module (GY-521) | 1 | ₹150 | Buy MPU6050 module (GY-521) |
| 0.96" SSD1306 OLED, I2C, 4-pin | 1 | ₹180 | Buy 0.96" SSD1306 OLED, I2C, 4-pin |
| Active buzzer module | 1 | ₹25 | Buy Active buzzer module |
| Jumper wires + small breadboard | 1 | ₹80 | Buy Jumper wires + small breadboard |
| Heavy base (tile or wooden block) and double-sided tape | 1 | ₹50 | Buy 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 R3 | Part · pin | Wire | Notes |
|---|---|---|---|
| A4 | MPU6050 (GY-521) · SDA | blue | |
| A5 | MPU6050 (GY-521) · SCL | yellow | |
| 5V | MPU6050 (GY-521) · VCC | red | The GY-521 has its own 3.3 V regulator and 3.3 V pull-ups |
| GND | MPU6050 (GY-521) · GND | black | AD0 left open = address 0x68 |
| SDA | SSD1306 OLED 0.96″ (128×64, I2C) · SDA | blue | The SDA header pin is the same signal as A4 |
| SCL | SSD1306 OLED 0.96″ (128×64, I2C) · SCL | yellow | The SCL header pin is the same signal as A5 |
| 5V | SSD1306 OLED 0.96″ (128×64, I2C) · VCC | red | |
| GND | SSD1306 OLED 0.96″ (128×64, I2C) · GND | black | |
| D8 | Active buzzer module (high-level trigger) · I/O | orange | |
| 5V | Active buzzer module (high-level trigger) · VCC | red | |
| GND | Active buzzer module (high-level trigger) · GND | black |
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
// 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));
}
}// 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.



