Seismograph on an Arduino Uno
An MPU6050 with gravity filtered out and a SW-420 vibration switch: live earthquake traces sweep an OLED with the peak shaking in g and its Mercalli intensity. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 25 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $37. On Mokxi they're free.
What you'll learn
- Sensors
- Displays
- Using the Accelerometer and gyroscope
- Using the Vibration sensor
- Difficulty: Advanced
- Time: About 25 minutes
- Board: Arduino Uno R3
- Parts: 5
How it works
An MPU6050 with gravity filtered out and a SW-420 vibration switch: live earthquake traces sweep an OLED with the peak shaking in g and its Mercalli intensity.
Arduino Uno R3 runs the sketch below. The circuit around it: Accelerometer and gyroscope, MPU6050, Vibration sensor, SW-420, OLED display, 128x64. pinMode sets LED_PIN as an output. digitalWrite drives LED_PIN HIGH. Serial.begin opens the serial port at 115200 baud. millis reads the time since power-up, so the sketch can time things without stopping.
Parts list
5 parts, plus the jumper wires. They retail for about $37; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Accelerometer and gyroscope, MPU6050In Desk Buddy Kit, Laser Sword Kit, Escape Box Kit.
- 1 × Vibration sensor, SW-420
- 1 × OLED display, 128x64In Fight Stick Kit, Pocket Pet Kit, Slot Machine Kit.
Kits that can build it
This starter kit has every part in the circuit. Each kit's page lists what else it can build.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Vibration sensor, SW-420 pin DO | Arduino Uno R3 pin 2 |
| Accelerometer and gyroscope, MPU6050 pin VCC | Arduino Uno R3 pin 5V |
| Vibration sensor, SW-420 pin VCC | Arduino Uno R3 pin 5V |
| OLED display, 128x64 pin VCC | Arduino Uno R3 pin 5V |
| Accelerometer and gyroscope, MPU6050 pin GND | Arduino Uno R3 pin GND |
| Accelerometer and gyroscope, MPU6050 pin AD0 | Arduino Uno R3 pin GND |
| Vibration sensor, SW-420 pin GND | Arduino Uno R3 pin GND |
| OLED display, 128x64 pin GND | Arduino Uno R3 pin GND |
| Accelerometer and gyroscope, MPU6050 pin SDA | Arduino Uno R3 pin A4 |
| OLED display, 128x64 pin SDA | Arduino Uno R3 pin A4 |
| Accelerometer and gyroscope, MPU6050 pin SCL | Arduino Uno R3 pin A5 |
| OLED display, 128x64 pin SCL | Arduino Uno R3 pin A5 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Seismograph: an MPU6050 accelerometer, a SW-420 vibration switch and an OLED.
//
// A4, A5 MPU6050 and OLED on one I2C bus (addresses 0x68 and 0x3C)
// pin 2 SW-420 DO, the vibration switch: goes LOW when it is shaken
// pin 13 the Uno's own LED, lit while an earthquake is going on
//
// # What a seismograph does
//
// A seismograph is a very sensitive accelerometer with a pen. It measures how
// hard the ground is being shaken, many times a second, and draws the
// wiggles. The MPU6050 reports acceleration in "g", where 1 g is gravity.
// Standing still on a table it reads 1 g straight down, which is no use for
// finding a shake, so the sketch keeps a slow running average of each axis
// (the part that does not change: gravity and any tilt) and subtracts it. What
// is left is the shaking.
//
// # Intensity is not magnitude
//
// The size of an earthquake at its source, its magnitude (Richter or moment
// magnitude), is the energy it released. It takes several instruments in
// different places to work out. One sensor cannot know it. What one sensor can
// measure is how hard the ground shook right HERE, the peak ground
// acceleration. That is what the screen shows as PEAK, in g, and the
// Modified Mercalli Intensity (MMI) it corresponds to, from I (not felt) to
// X and up (destroyed buildings). The table is the US Geological Survey's
// instrumental intensity: 0.0017 g is about the weakest shake a person
// notices, 0.092 g makes things fall off shelves, over 1 g throws you.
//
// # The traces
//
// Two strips: the top is the sideways (X) shaking, the bottom is forwards and
// back (Y), each with a dotted line at zero. The pen sweeps left to right
// every six seconds, one column at a time, and rubs out the old picture just
// ahead of itself, like the paper drum of a real seismograph. Every column is
// the highest and lowest reading in its 50 milliseconds, drawn as one line, so
// a short sharp jolt is never missed between two columns.
//
// The SW-420 is the burglar alarm of the circuit. It is a spring and a metal
// pin that touch when shaken, which pulls DO low. An interrupt notices that
// even while the sketch is busy drawing, and counts it as a TRIP. A trip, or
// the MPU6050 reading more than 0.02 g, starts an EARTHQUAKE: the sketch
// records the peak and the time until the ground has been quiet for a second
// and a half.
//
// The Serial Plotter shows the same traces live: x, y, z and peak, in g.
//
// Tilt the MPU6050's pitch and roll back and forth to shake the ground.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const uint8_t MPU = 0x68;
const int TRIP_PIN = 2;
const int LED_PIN = 13;
const float G_PER_COUNT = 1.0 / 16384.0; // +-2 g range: 16384 counts per g
const float START_G = 0.02; // shaking harder than this starts an earthquake
const float QUIET_G = 0.008; // under this is quiet
const unsigned long QUIET_MS = 1500; // quiet for this long ends it
// The screen: two strips of 22 rows under the text, ten pixels each way from
// the centre line, which is 0.15 g at full scale.
const int CENTRE_X = 31;
const int CENTRE_Y = 52;
const int SPAN = 10;
const float PIXELS_PER_G = SPAN / 0.15;
Adafruit_SSD1306 display(128, 64, &Wire, -1);
volatile uint8_t trips = 0;
volatile unsigned long lastTripAt = 0;
uint8_t seenTrips = 0;
float base[3]; // the slow average: gravity and tilt
bool haveBase = false;
float dyn[3]; // what is left: the shaking
float mag = 0; // its size, in g
bool quake = false;
float peak = 0; // the biggest shaking of the last (or current) earthquake, g
unsigned long quakeStart = 0;
unsigned long quakeLength = 0; // ms, final once it has ended
unsigned long lastLoud = 0;
int column = 0;
int lowest[2]; // the lowest and highest pixel offset seen this column
int highest[2];
int samplesInColumn = 0;
int columnsSinceShown = 0;
unsigned long lastSample = 0;
unsigned long lastPlot = 0;
// The spring in a SW-420 bounces: one knock makes a burst of pulses, so only
// the first pulse in a quarter of a second counts as a trip.
void onTrip() {
unsigned long now = millis();
if (now - lastTripAt > 250 || trips == 0) {
trips++;
lastTripAt = now;
}
}
bool readMpu() {
// The running average follows with a time constant of 0.6 s, however long
// the loop took since the last reading (drawing the screen takes a while).
static unsigned long lastRead = 0;
unsigned long now = millis();
float follow = (now - lastRead) / 600.0;
if (follow > 1) follow = 1;
lastRead = now;
Wire.beginTransmission(MPU);
Wire.write(0x3B); // ACCEL_XOUT_H: six bytes follow, X, Y, Z, high byte first
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(MPU, (uint8_t)6, (uint8_t)true) != 6) return false;
for (int i = 0; i < 3; i++) {
int16_t raw = (Wire.read() << 8) | Wire.read();
float g = raw * G_PER_COUNT;
if (!haveBase) base[i] = g;
// A slow average: it follows a tilt but not a shake, which comes and goes
// too fast for it.
base[i] += (g - base[i]) * follow;
dyn[i] = g - base[i];
}
haveBase = true;
mag = sqrt(dyn[0] * dyn[0] + dyn[1] * dyn[1] + dyn[2] * dyn[2]);
return true;
}
// The Modified Mercalli Intensity for a peak ground acceleration in g, after
// the USGS instrumental intensity table.
const char *intensity(float g) {
if (g < 0.0017) return "I";
if (g < 0.014) return "II-III";
if (g < 0.039) return "IV";
if (g < 0.092) return "V";
if (g < 0.18) return "VI";
if (g < 0.34) return "VII";
if (g < 0.65) return "VIII";
if (g < 1.24) return "IX";
return "X+";
}
int pixelsFor(float g) {
int p = (int)(g * PIXELS_PER_G);
if (p > SPAN) p = SPAN;
if (p < -SPAN) p = -SPAN;
return p;
}
void drawText() {
display.fillRect(0, 0, 128, 19, SSD1306_BLACK);
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
// Line one: the biggest shaking and how many times the switch has tripped.
display.setCursor(0, 0);
display.print("PEAK ");
display.print(peak, 3);
display.print("g");
display.setCursor(90, 0);
display.print("TRIP ");
display.print(seenTrips);
// Line two: what is going on, and the Mercalli intensity of the peak.
display.setCursor(0, 10);
if (quake) {
display.print("QUAKE ");
display.print((millis() - quakeStart) / 100 / 10.0, 1);
display.print("s");
} else if (quakeLength > 0) {
display.print("LAST ");
display.print(quakeLength / 100 / 10.0, 1);
display.print("s");
} else {
display.print("ARMED");
}
display.setCursor(68, 10);
display.print("MMI ");
display.print(intensity(peak));
}
void startQuake() {
quake = true;
peak = 0;
quakeStart = millis();
digitalWrite(LED_PIN, HIGH);
Serial.println("EARTHQUAKE start");
}
void endQuake() {
quake = false;
quakeLength = lastLoud - quakeStart;
digitalWrite(LED_PIN, LOW);
Serial.print("EARTHQUAKE end peak ");
Serial.print(peak, 3);
Serial.print(" g MMI ");
Serial.print(intensity(peak));
Serial.print(" lasted ");
Serial.print(quakeLength / 100 / 10.0, 1);
Serial.println(" s");
}
// Draw the column just finished, rub out the three ahead of it, and move on.
void finishColumn() {
for (int a = 0; a < 2; a++) {
int centre = a == 0 ? CENTRE_X : CENTRE_Y;
for (int k = 1; k <= 3; k++) {
int ahead = (column + k) % 128;
display.drawFastVLine(ahead, centre - SPAN, 2 * SPAN + 1, SSD1306_BLACK);
if ((ahead & 3) == 0) display.drawPixel(ahead, centre, SSD1306_WHITE);
}
// up on screen is the positive side, so higher values have smaller y
int top = centre - highest[a];
int height = highest[a] - lowest[a] + 1;
display.drawFastVLine(column, centre - SPAN, 2 * SPAN + 1, SSD1306_BLACK);
if ((column & 3) == 0) display.drawPixel(column, centre, SSD1306_WHITE);
display.drawFastVLine(column, top, height, SSD1306_WHITE);
lowest[a] = 127;
highest[a] = -127;
}
column = (column + 1) % 128;
samplesInColumn = 0;
// Each display() sends only the 8-column blocks that changed, but it takes
// time, and the accelerometer is not read while it runs. Showing every
// fourth column keeps the gaps short.
if (++columnsSinceShown >= 4) {
columnsSinceShown = 0;
drawText();
display.display();
}
}
void sample() {
if (!readMpu()) return;
unsigned long now = millis();
// A trip from the vibration switch.
uint8_t t = trips;
bool tripped = t != seenTrips;
seenTrips = t;
if (tripped) {
Serial.print("TRIP ");
Serial.println(t);
}
if (mag > START_G || tripped) {
if (!quake) startQuake();
lastLoud = now;
} else if (mag > QUIET_G) {
lastLoud = now; // still shaking, just below the start threshold
}
if (quake) {
if (mag > peak) peak = mag;
if (now - lastLoud > QUIET_MS) endQuake();
}
// This sample into the column's highest and lowest, for X and Y.
for (int a = 0; a < 2; a++) {
int p = pixelsFor(dyn[a]);
if (p < lowest[a]) lowest[a] = p;
if (p > highest[a]) highest[a] = p;
}
if (++samplesInColumn >= 5) finishColumn();
}
void setup() {
Serial.begin(115200);
Serial.println("Mokxi Uno: seismograph");
pinMode(LED_PIN, OUTPUT);
pinMode(TRIP_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(TRIP_PIN), onTrip, FALLING);
Wire.begin();
Wire.beginTransmission(MPU); // wake the MPU6050: it starts asleep
Wire.write(0x6B); // PWR_MGMT_1
Wire.write(0);
Wire.endTransmission();
Wire.beginTransmission(MPU); // accelerometer range +-2 g
Wire.write(0x1C); // ACCEL_CONFIG
Wire.write(0);
Wire.endTransmission();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
for (int a = 0; a < 2; a++) {
lowest[a] = 127;
highest[a] = -127;
int centre = a == 0 ? CENTRE_X : CENTRE_Y;
for (int x = 0; x < 128; x += 4) display.drawPixel(x, centre, SSD1306_WHITE);
}
drawText();
display.display();
}
void loop() {
unsigned long now = millis();
if (now - lastSample >= 10) { // 100 readings a second
lastSample = now;
sample();
}
if (now - lastPlot >= 100) {
lastPlot = now;
Serial.print("x:");
Serial.print(dyn[0], 3);
Serial.print(" y:");
Serial.print(dyn[1], 3);
Serial.print(" z:");
Serial.print(dyn[2], 3);
Serial.print(" peak:");
Serial.println(peak, 3);
}
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Runs once when the board starts. It gets the pins and libraries ready.
void setup() {
Serial.begin(115200);
Serial.println("Mokxi Uno: seismograph");
pinMode(LED_PIN, OUTPUT);
pinMode(TRIP_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(TRIP_PIN), onTrip, FALLING);
Wire.begin();
Wire.beginTransmission(MPU); // wake the MPU6050: it starts asleep
Wire.write(0x6B); // PWR_MGMT_1
Wire.write(0);
Wire.endTransmission();
Wire.beginTransmission(MPU); // accelerometer range +-2 g
Wire.write(0x1C); // ACCEL_CONFIG
Wire.write(0);
Wire.endTransmission();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.clearDisplay();
for (int a = 0; a < 2; a++) {
lowest[a] = 127;
highest[a] = -127;
int centre = a == 0 ? CENTRE_X : CENTRE_Y;
for (int x = 0; x < 128; x += 4) display.drawPixel(x, centre, SSD1306_WHITE);
}
drawText();
display.display();
}loop sketch.ino
Runs over and over for as long as the board has power. This is where the project does its work.
void loop() {
unsigned long now = millis();
if (now - lastSample >= 10) { // 100 readings a second
lastSample = now;
sample();
}
if (now - lastPlot >= 100) {
lastPlot = now;
Serial.print("x:");
Serial.print(dyn[0], 3);
Serial.print(" y:");
Serial.print(dyn[1], 3);
Serial.print(" z:");
Serial.print(dyn[2], 3);
Serial.print(" peak:");
Serial.println(peak, 3);
}
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The sensor is not found or reads all zeros. Accelerometer and gyroscope
Check SDA and SCL (A4 and A5 on an Uno), power, and ground. The I2C address is 0x68, or 0x69 if AD0 is high. The chip starts in sleep mode, so write 0 to the power register PWR_MGMT_1. - The values look wrong or the angles drift. Accelerometer and gyroscope
Raw readings are counts, not g or degrees per second. Divide by the scale for your range (16384 per g at 2 g). Gyro angles drift, so blend with the accelerometer using a filter. - It reads the same all the time, or false triggers. A pot sets the comparator threshold and the sensor is a very sensitive switch. Vibration sensor
Turn the pot until the LED just goes off at rest, then a tap trips it. Read the digital output DO, and count one edge per knock instead of a level. - The output is wired to a 5 V logic level on a 3.3 V board, or power is missing. Vibration sensor
Power the module from 3.3 V or 5 V to match your board's logic level. Join VCC and GND, then connect DO to a digital input. Use an interrupt for fast knocks. - The OLED stays blank. OLED display
Check SDA and SCL are not swapped, and power it with 3.3 V or 5 V as its board allows. Most use address 0x3C, some 0x3D. Call display.begin() with the matching address. - Text never shows up even though the code runs. OLED display
Drawing only writes to a buffer. Call display.display() after drawing to push it out. Also set a text color with setTextColor(WHITE) and clear the buffer between frames. - Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected. - Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.