Skip to content
Buy 10+ on select items — save 10% auto-applied
Free US shipping on orders $35+
Order by 3pm ET — ships same-day from the US
Worldwide shipping via DHL
Skip to main content

Arduino Nano MPU6050: OLED Seismogram STA/LTA Alarm

October 25, 2026 3 views

Arduino Nano MPU6050: OLED Seismogram STA/LTA Alarm | ShillehTek
Project

Build an Arduino Nano MPU6050 seismometer with a live SSD1306 OLED seismogram, STA/LTA alarm buzzer, and PC CSV logger using ShillehTek parts.

1 hr Intermediate7 parts

Project Overview

Arduino Nano MPU6050 Seismometer: turn an MPU6050 accelerometer and an Arduino Nano into a shake detector that draws a live seismogram on an OLED, sounds an alarm using the same STA/LTA trigger that real seismic networks use, and streams every reading to your PC for logging.

  • Time: 1 to 2 hours
  • Skill level: Intermediate
  • What you will build: A floor-mounted vibration and earthquake monitor with a scrolling 25-second trace, an adjustable trigger, a buzzer alarm, and a CSV data logger.
Arduino Nano and MPU6050 seismometer build mounted on a rigid base with a sensitivity slider and leveling bubbles
The reference build by Mirko Pavleski: an MPU6050 module, two Nanos, a sensitivity trimmer and two bubble levels on a rigid base. This guide uses one Nano and adds an OLED trace, a buzzer and a PC logger.

Parts List

From ShillehTek

External

  • A heavy, rigid base: a ceramic floor tile, a paving slab or a thick piece of hardwood
  • Double-sided foam tape, hot glue or screws to fix the sensor and board to the base
  • USB cable and a PC (for logging) or any 5 V USB supply (stand-alone use)
  • Optional: a small bubble level

Note: the MPU6050 has roughly 1 mg of noise per axis, so this detects felt shaking: footsteps, slammed doors, passing trucks and earthquakes strong enough to feel nearby. It will not record distant quakes the way a broadband seismometer does.

Step-by-Step Guide

Step 1 - Understand how the detector works

Goal: Know what the sketch measures before you tune it.

What to do: Read through the signal chain once. It is the same idea professional networks use, scaled down to a Nano:

  • 100 samples per second through the FIFO. The MPU6050 is set to a 5 Hz low-pass filter and a 100 Hz sample rate, and it queues readings in its 1024-byte FIFO. The Nano drains the queue every loop, so no samples are lost while the OLED refreshes.
  • High-pass filter at 0.5 Hz. A first-order filter on each axis removes gravity, tilt and slow thermal drift, leaving only vibration in mg (thousandths of g).
  • STA/LTA trigger. The short-term average (0.5 s) of the 3-axis amplitude is compared with the long-term average (30 s). An event starts when STA/LTA rises above the threshold you set with the slide pot (2.0 to 10.0) and STA is at least 2 mg. It ends when the ratio stays below 1.5 for one second. The LTA is frozen during an event so a long quake does not raise its own baseline.
  • Long shaking. Events are capped at two minutes. After a cap, triggering pauses for 30 seconds so the LTA can learn the new background (a washing machine, for example).

Expected result: You know that a higher slide-pot setting means a less sensitive trigger, and that the first 30 seconds after power-up are spent learning the room's noise.

Step 2 - Wire the circuit

Goal: Connect the sensor, display, slider and buzzer to the Nano.

What to do: The MPU6050 (address 0x68) and the OLED (address 0x3C) share the I2C bus. The Nano's on-board LED on D13 doubles as the alarm light.

  • MPU6050: VCC → 5V, GND → GND, SDA → A4, SCL → A5 (leave AD0 and INT unconnected)
  • OLED: VCC → 5V, GND → GND, SDA → A4, SCL → A5
  • Slide pot: VCC → 5V, GND → GND, output (OTA) → A0
  • KY-006 buzzer: S → D8, − → GND
Wiring diagram showing Arduino Nano connected to an MPU6050 over I2C plus a sensitivity potentiometer and an alarm output
The original two-board wiring for reference: the MPU6050 on A4/A5, a trimmer on the analog pins, an alarm LED, and a second Nano that fed a PC program. Here the second board is replaced by the USB serial logger in Step 6.

Expected result: Everything is powered from the Nano's 5V pin, and both I2C modules sit on A4/A5.

Step 3 - Mount the sensor properly

Goal: Make the sensor feel the ground, not the furniture.

What to do: Placement matters more than code.

  • Fix the MPU6050 flat (chip facing up, so its Z axis is vertical) to the heavy base with foam tape or screws. A loose sensor rattles and produces fake events.
  • Put the base on the floor, ideally a concrete slab or ground floor, away from doors, stairs and walkways. A desk amplifies footsteps and keyboard taps.
  • Level the base with a bubble level and tape the USB cable down so it cannot tug on the sensor.

Expected result: The sensor sits rigidly on a heavy base, on the floor, with the cable strain-relieved.

Step 4 - Upload the seismometer sketch

Goal: Run the FIFO reader, filter, trigger and display.

What to do: In the Arduino IDE, install Adafruit SSD1306 and Adafruit GFX from the Library Manager. Select Arduino Nano (try ATmega328P (Old Bootloader) if the upload fails) and upload this sketch. It talks to the MPU6050 registers directly, so no MPU library is needed.

Code:

// ShillehTek Nano seismometer: MPU6050 (FIFO, 100 Hz) + SSD1306 seismogram + STA/LTA alarm
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

const uint8_t MPU_ADDR   = 0x68;
const uint8_t OLED_ADDR  = 0x3C;
const uint8_t POT_PIN    = A0;           // slide pot: trigger ratio 2.0 ... 10.0
const uint8_t BUZZER_PIN = 8;            // KY-006 passive buzzer
const uint8_t ALARM_LED  = LED_BUILTIN;  // Nano on-board LED (D13)

const float DT          = 0.01f;         // 100 Hz sample rate (set by SMPLRT_DIV)
const float COUNTS_PER_MG = 16.384f;     // +/-2 g range = 16384 counts per g
const float HP_CUTOFF_HZ = 0.5f;         // removes gravity, tilt and slow drift
const float STA_SEC     = 0.5f;          // short-term average window
const float LTA_SEC     = 30.0f;         // long-term average window
const float RATIO_OFF   = 1.5f;          // event ends below this STA/LTA ratio...
const uint16_t QUIET_SAMPLES = 100;      // ...for 1 s in a row
const float MIN_STA_MG  = 2.0f;          // never trigger on tiny absolute levels
const uint16_t WARMUP_SAMPLES = 3000;    // 30 s to learn the background noise
const uint8_t SAMPLES_PER_COLUMN = 20;   // one OLED column = 200 ms (128 columns = 25.6 s)
const uint8_t CSV_DECIMATE = 4;          // serial CSV at 25 Hz

Adafruit_SSD1306 display(128, 64, &Wire, -1, 400000UL, 400000UL);
bool haveDisplay = false;

// High-pass filter state (one per axis)
float hpA;                               // filter coefficient, set in setup()
float prevIn[3], hpOut[3];
bool hpPrimed = false;

// Detector state
float sta = 0, lta = 0, ratioOn = 4.0f;
uint32_t sampleCount = 0;
bool eventActive = false;
uint32_t eventStart = 0, eventEnd = 0;
uint16_t quietCount = 0;
float peakH = 0, peakV = 0, peakRatio = 0;
bool haveLastEvent = false;
uint32_t holdoffUntil = 0;              // no new triggers until the LTA re-learns
float lastPeakH = 0, lastPeakV = 0;

// Seismogram: peak 3-axis amplitude per column, 0.5 mg units
uint8_t trace[128];
uint8_t traceHead = 0;
uint8_t columnSamples = 0;
float columnPeak = 0;
bool columnReady = false;
unsigned long lastBeep = 0;

void mpuWrite(uint8_t reg, uint8_t val) {
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(reg);
  Wire.write(val);
  Wire.endTransmission();
}

uint16_t fifoCount() {
  Wire.beginTransmission(MPU_ADDR);
  Wire.write(0x72);                      // FIFO_COUNT_H
  if (Wire.endTransmission(false) != 0) return 0;
  if (Wire.requestFrom(MPU_ADDR, (uint8_t)2) != 2) return 0;
  uint8_t hi = Wire.read();
  uint8_t lo = Wire.read();
  return ((uint16_t)hi << 8) | lo;
}

void fifoReset() {
  mpuWrite(0x6A, 0x04);                  // USER_CTRL: FIFO_RESET
  mpuWrite(0x6A, 0x40);                  // USER_CTRL: FIFO_EN
}

void setupMpu() {
  mpuWrite(0x6B, 0x01);                  // PWR_MGMT_1: wake up, PLL clock
  delay(100);
  mpuWrite(0x1A, 0x06);                  // CONFIG: DLPF 5 Hz
  mpuWrite(0x19, 0x09);                  // SMPLRT_DIV: 1 kHz / (1 + 9) = 100 Hz
  mpuWrite(0x1C, 0x00);                  // ACCEL_CONFIG: +/-2 g
  mpuWrite(0x23, 0x08);                  // FIFO_EN: accelerometer X, Y, Z
  fifoReset();
}

const char *shakingLabel(float mg) {     // USGS ShakeMap PGA bands (1 %g = 10 mg)
  if (mg < 1.7f)  return "NOT FELT";
  if (mg < 14.0f) return "WEAK";
  if (mg < 39.0f) return "LIGHT";
  if (mg < 92.0f) return "MODERATE";
  if (mg < 180.0f) return "STRONG";
  return "V.STRONG+";
}

void endEvent(bool capped) {
  eventActive = false;
  if (capped) holdoffUntil = sampleCount + WARMUP_SAMPLES;   // long shaking: re-learn background for 30 s
  eventEnd = sampleCount;
  haveLastEvent = true;
  lastPeakH = peakH;
  lastPeakV = peakV;
  digitalWrite(ALARM_LED, LOW);
  Serial.print(F("# EVENT t="));
  Serial.print(eventStart * DT, 2);
  Serial.print(F("s dur="));
  Serial.print((eventEnd - eventStart) * DT, 1);
  Serial.print(F("s peakH="));
  Serial.print(peakH, 1);
  Serial.print(F("mg peakV="));
  Serial.print(peakV, 1);
  Serial.print(F("mg ratio="));
  Serial.print(peakRatio, 1);
  Serial.print(F(" shaking="));
  Serial.println(shakingLabel(peakH));
}

void processSample(int16_t ax, int16_t ay, int16_t az) {
  float in[3] = { (float)ax, (float)ay, (float)az };
  float mg[3];
  if (!hpPrimed) {                       // start the filter at the current reading (no step)
    for (uint8_t i = 0; i < 3; i++) { prevIn[i] = in[i]; hpOut[i] = 0; }
    hpPrimed = true;
  }
  for (uint8_t i = 0; i < 3; i++) {
    hpOut[i] = hpA * (hpOut[i] + in[i] - prevIn[i]);
    prevIn[i] = in[i];
    mg[i] = hpOut[i] / COUNTS_PER_MG;
  }
  float h = sqrt(mg[0] * mg[0] + mg[1] * mg[1]);   // horizontal
  float v = fabs(mg[2]);                            // vertical (sensor Z up)
  float total = sqrt(h * h + v * v);
  sampleCount++;

  // STA/LTA trigger on the 3-axis amplitude
  sta += (total - sta) * (DT / STA_SEC);
  if (sampleCount <= WARMUP_SAMPLES) {
    lta += (total - lta) / (float)sampleCount;      // plain average while warming up
  } else if (!eventActive) {
    lta += (total - lta) * (DT / LTA_SEC);          // LTA is frozen during events
  }
  float ratio = sta / (lta > 0.05f ? lta : 0.05f);

  if (sampleCount > WARMUP_SAMPLES) {
    if (!eventActive) {
      if (ratio >= ratioOn && sta >= MIN_STA_MG && sampleCount >= holdoffUntil) {
        eventActive = true;
        eventStart = sampleCount;
        peakH = h; peakV = v; peakRatio = ratio;
        quietCount = 0;
        digitalWrite(ALARM_LED, HIGH);
      }
    } else {
      if (h > peakH) peakH = h;
      if (v > peakV) peakV = v;
      if (ratio > peakRatio) peakRatio = ratio;
      if (ratio < RATIO_OFF) {
        if (++quietCount >= QUIET_SAMPLES) endEvent(false);
      } else {
        quietCount = 0;
      }
      if (eventActive && sampleCount - eventStart > 12000) endEvent(true);   // 2 min cap
    }
  }

  // Seismogram column
  if (total > columnPeak) columnPeak = total;
  if (++columnSamples >= SAMPLES_PER_COLUMN) {
    float units = columnPeak * 2.0f;                // 0.5 mg units
    trace[traceHead] = units > 255.0f ? 255 : (uint8_t)units;
    traceHead = (traceHead + 1) & 127;
    columnSamples = 0;
    columnPeak = 0;
    columnReady = true;
  }

  // CSV for the Serial Plotter or the PC logger
  if (sampleCount % CSV_DECIMATE == 0) {
    Serial.print(sampleCount * DT, 2); Serial.print(',');
    Serial.print(mg[0], 2); Serial.print(',');
    Serial.print(mg[1], 2); Serial.print(',');
    Serial.print(mg[2], 2); Serial.print(',');
    Serial.print(sta, 2); Serial.print(',');
    Serial.print(lta, 2); Serial.print(',');
    Serial.println(eventActive ? 1 : 0);
  }
}

void drainFifo() {
  uint16_t count = fifoCount();
  if (count > 1020) {                    // overflowed: data no longer aligned
    fifoReset();
    Serial.println(F("# FIFO overflow, reset"));
    return;
  }
  while (count >= 6) {
    uint8_t samples = count / 6;
    if (samples > 5) samples = 5;        // AVR Wire buffer is 32 bytes
    Wire.beginTransmission(MPU_ADDR);
    Wire.write(0x74);                    // FIFO_R_W
    if (Wire.endTransmission(false) != 0) return;
    uint8_t bytes = samples * 6;
    if (Wire.requestFrom(MPU_ADDR, bytes) != bytes) return;
    for (uint8_t s = 0; s < samples; s++) {
      uint8_t xh = Wire.read(); uint8_t xl = Wire.read();
      uint8_t yh = Wire.read(); uint8_t yl = Wire.read();
      uint8_t zh = Wire.read(); uint8_t zl = Wire.read();
      processSample((int16_t)(((uint16_t)xh << 8) | xl),
                    (int16_t)(((uint16_t)yh << 8) | yl),
                    (int16_t)(((uint16_t)zh << 8) | zl));
    }
    count -= bytes;
  }
}

void drawScreen() {
  uint8_t maxv = 0;
  for (uint8_t i = 0; i < 128; i++) if (trace[i] > maxv) maxv = trace[i];
  const uint8_t scales[] = { 4, 10, 20, 40, 100, 255 };    // 2, 5, 10, 20, 50, 127 mg
  uint8_t fs = 255;
  for (uint8_t i = 0; i < sizeof(scales); i++) {
    if (maxv <= scales[i]) { fs = scales[i]; break; }
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.print(F("R"));
  display.print(sta / (lta > 0.05f ? lta : 0.05f), 1);
  display.print(F(" TRIG"));
  display.print(ratioOn, 1);
  display.print(F(" FS"));
  display.print(fs / 2);
  display.print(F("mg"));
  for (uint8_t x = 0; x < 128; x++) {
    uint8_t value = trace[(traceHead + x) & 127];          // oldest on the left
    uint8_t half = (uint16_t)value * 20 / fs;
    if (half > 20) half = 20;
    display.drawFastVLine(x, 33 - half, half * 2 + 1, SSD1306_WHITE);
  }
  display.setCursor(0, 56);
  if (sampleCount <= WARMUP_SAMPLES) {
    display.print(F("Learning noise "));
    display.print((WARMUP_SAMPLES - sampleCount) / 100);
    display.print(F("s"));
  } else if (eventActive) {
    display.print(F("EVENT "));
    display.print(peakH, 1);
    display.print(F("mg "));
    display.print((sampleCount - eventStart) / 100);
    display.print(F("s"));
  } else if (haveLastEvent) {
    display.print(F("Last "));
    display.print(lastPeakH, 1);
    display.print(F("mg "));
    display.print(shakingLabel(lastPeakH));
  } else {
    display.print(F("Quiet  LTA "));
    display.print(lta, 1);
    display.print(F("mg"));
  }
  display.display();
}

void setup() {
  Serial.begin(115200);
  pinMode(ALARM_LED, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  Wire.begin();
  Wire.setClock(400000);
  float rc = 1.0f / (2.0f * PI * HP_CUTOFF_HZ);
  hpA = rc / (rc + DT);
  haveDisplay = display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  if (!haveDisplay) Serial.println(F("# OLED not found, logging to serial only"));
  setupMpu();
  Serial.println(F("t_s,x_mg,y_mg,z_mg,sta_mg,lta_mg,event"));
}

void loop() {
  drainFifo();
  if (columnReady) {
    columnReady = false;
    ratioOn = 2.0f + analogRead(POT_PIN) * (8.0f / 1023.0f);
    if (haveDisplay) drawScreen();
  }
  if (eventActive && millis() - lastBeep >= 500) {
    lastBeep = millis();
    tone(BUZZER_PIN, 2000, 80);
  }
}

Expected result: The OLED shows "Learning noise 30s" counting down, then switches to "Quiet" with the current LTA. The Serial Monitor at 115200 baud prints CSV lines 25 times per second.

Step 5 - Read the display and tune the trigger

Goal: Confirm detection and pick a sensible sensitivity.

What to do: The top line shows the live STA/LTA ratio (R), the trigger level from the slider (TRIG) and the auto-selected full scale of the trace (2 to 127 mg). Each column of the trace is the peak amplitude over 200 ms, so the screen shows the last 25.6 seconds.

Stamp on the floor a metre away or close a door: the trace spikes and, if the ratio crosses TRIG, the LED lights, the buzzer chirps every half second and the bottom line switches to "EVENT". Open Tools → Serial Plotter to watch the x, y and z columns live.

Start with the slider near the middle (about 6). Move it toward 2 for more sensitivity in a quiet basement, or toward 10 in a busy house.

Expected result: Background noise keeps R near 1.0, deliberate taps trigger short events, and each event prints a summary line such as # EVENT t=60.73s dur=4.0s peakH=22.4mg peakV=7.3mg ratio=8.2 shaking=LIGHT.

Step 6 - Log every reading on your PC

Goal: Keep a timestamped record you can graph later.

What to do: Install Python 3 and pyserial (pip install pyserial), close the Arduino Serial Monitor, and run the logger with your port, for example python seismo_logger.py COM5 on Windows or python seismo_logger.py /dev/cu.usbserial-1410 on a Mac. It writes one CSV per day with wall-clock times and appends event summaries to seismo_events.log.

Code:

#!/usr/bin/env python3
"""Log the Nano seismometer's serial CSV to daily files with wall-clock timestamps.

Usage: python seismo_logger.py COM5          (Windows)
       python seismo_logger.py /dev/ttyUSB0  (Linux / macOS: /dev/cu.usbserial-*)
Requires: pip install pyserial
"""
import csv
import datetime
import sys

import serial

PORT = sys.argv[1] if len(sys.argv) > 1 else "/dev/ttyUSB0"
FIELDS = ["time", "t_s", "x_mg", "y_mg", "z_mg", "sta_mg", "lta_mg", "event"]


def open_day_file(day):
    f = open(f"seismo_{day}.csv", "a", newline="", buffering=1)   # line-buffered: nothing lost on Ctrl+C
    w = csv.writer(f)
    if f.tell() == 0:
        w.writerow(FIELDS)
    return f, w


def main():
    current_day, f, writer = None, None, None
    with serial.Serial(PORT, 115200, timeout=2) as ser, open("seismo_events.log", "a") as events:
        print(f"Logging {PORT} ... Ctrl+C to stop")
        while True:
            line = ser.readline().decode("ascii", errors="ignore").strip()
            if not line:
                continue
            now = datetime.datetime.now()
            if line.startswith("#"):                 # event summaries and status lines
                stamp = now.isoformat(timespec="seconds")
                print(stamp, line)
                events.write(f"{stamp} {line}\n")
                events.flush()
                continue
            parts = line.split(",")
            if len(parts) != 7 or parts[0] == "t_s":
                continue
            day = now.date().isoformat()
            if day != current_day:                   # start a new file at midnight
                if f:
                    f.close()
                f, writer = open_day_file(day)
                current_day = day
            writer.writerow([now.isoformat(timespec="milliseconds")] + parts)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("Stopped.")

Expected result: Files named like seismo_2026-10-25.csv grow by 25 rows per second, ready for Excel, pandas or Grafana.

Step 7 - Interpret the numbers

Goal: Put an event's peak into context.

What to do: The sketch labels each event using the peak-acceleration bands from the USGS ShakeMap scale (1 %g = 10 mg). Remember that the label describes ground shaking; a bump right next to the sensor will read high locally.

  • Below 1.7 mg: not felt (also the sensor's noise floor)
  • 1.7 to 14 mg: weak (intensity II to III)
  • 14 to 39 mg: light (IV)
  • 39 to 92 mg: moderate (V)
  • 92 to 180 mg: strong (VI)
  • Above 180 mg: very strong or worse (VII+)

Expected result: You can tell a slammed door (a short, sharp spike) from sustained shaking (a long event with a growing then fading trace), and you have numbers to compare against official reports if a real quake is felt.

Conclusion

You built an Arduino Nano seismometer that reads an MPU6050 at 100 samples per second through its FIFO, filters out gravity, draws a live seismogram on an SSD1306 OLED, triggers a buzzer alarm with an STA/LTA detector, and logs every reading to your PC.

Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help customizing this project or building something for your product, check out our IoT consulting services.

Credits

All photos and images in this tutorial are credited to Mirko Pavleski (mircemk) on Hackster.io. The original guide by Mirko Pavleski served as the reference for this ShillehTek version. We thank them for their excellent work in the maker community.

Parts for this build

Everything used in this tutorial. Uncheck what you already have.

All 7 in stock
0 parts selected $0.00