Building a reliable vibration detector Arduino project requires moving past cheap, bouncing tilt switches. If you need to monitor machinery health, detect specific knock frequencies, or measure actual G-force impacts, you need a true 3-axis accelerometer. This guide uses the Arduino Nano v3 (ATmega328P) paired with the ADXL345 digital accelerometer via I2C. We will bypass heavy third-party libraries and read the sensor registers directly using the native Wire.h library, giving you total control over the sampling rate and interrupt thresholds while keeping the compiled footprint under 15KB.

Sensor Selection: Why the ADXL345 Beats the SW-420

Most beginner tutorials point you toward the SW-420 vibration switch or a raw piezo disk. While these cost pennies, they lack the data density required for real-world diagnostics. The SW-420 is essentially a mechanical spring that shorts a contact; it gives you a binary "yes/no" but no magnitude, and it suffers from severe contact bounce. The ADXL345 provides 13-bit resolution up to ±16g, allowing you to calculate the exact vector magnitude of a vibration event.

Table 1: Vibration Sensor Comparison for Embedded Projects
Sensor Module Interface Noise Density Typical Price (2026) Best Application
SW-420 Digital (GPIO) N/A (Mechanical) $0.50 Basic knock alarms, binary wake-up triggers
Piezo Disk Analog (ADC) High (Susceptible to EMI) $1.00 High-frequency acoustic resonance, drum triggers
ADXL345 (GY-291) I2C / SPI 120 µg/√Hz (Very Low) $4.50 Precision machinery monitoring, impact magnitude
MPU6050 I2C 400 µg/√Hz (Medium) $3.50 Motion tracking, IMU fusion (overkill for pure vibration)

Hardware BOM and Pin Mapping

This build assumes you are using the widely available GY-291 breakout board for the ADXL345. Because the Arduino Nano v3 operates at 5V logic and the ADXL345 is strictly a 3.3V device, we are including a bidirectional logic level shifter to prevent long-term degradation of the sensor's I2C pins.

Bench Note: Many cheap GY-291 boards have an onboard 3.3V LDO regulator for power, but their I2C pins are not 5V tolerant. While directly connecting Nano 5V pins to the sensor often "works" on the bench, it violates the Analog Devices ADXL345 datasheet absolute maximum ratings. Use a level shifter for a permanent installation.

Parts List

  • Microcontroller: Arduino Nano v3 (ATmega328P, CH340G or FT232RL USB-UART)
  • Sensor: GY-291 ADXL345 Breakout Board
  • Level Shifter: BSS138 Bidirectional Logic Level Converter (4-channel)
  • Output: 5V Active Piezo Buzzer (e.g., TMB12A05)
  • Passives: 2x 4.7kΩ resistors (for I2C pull-ups on the 3.3V side)

Pin Mapping Table

Arduino Nano v3 Pin Logic Level Shifter ADXL345 (GY-291) Pin Function
5VHV (High Voltage)VCCPower (5V to LDO)
GNDGNDGNDCommon Ground
3.3VLV (Low Voltage)-Reference for LV side
A4 (SDA)HV1SDA (via LV1)I2C Data
A5 (SCL)HV2SCL (via LV2)I2C Clock
D8--Buzzer Signal (Direct)
D13--Onboard LED Indicator

Assembly and I2C Wiring Steps

  1. Power the Level Shifter: Connect the Nano 5V to the HV pin and Nano 3.3V to the LV pin on the BSS138 module. Tie all GND pins together.
  2. Wire the I2C Bus: Route Nano A4 to HV1, and HV1's corresponding LV1 to the ADXL345 SDA. Repeat for A5 (SCL) through HV2/LV2.
  3. Install Pull-ups: Solder or breadboard a 4.7kΩ resistor between the LV1 (SDA) line and the 3.3V LV rail. Repeat for LV2 (SCL). The Arduino Wire library relies on these pull-ups to achieve clean rising edges at 400kHz.
  4. Set the I2C Address: Leave the SDO pin on the ADXL345 floating or tie it to GND. This sets the 7-bit I2C address to 0x53. If you tie SDO to 3.3V, the address shifts to 0x1D.
  5. Connect the Output: Wire the positive leg of the active piezo buzzer to Nano D8, and the negative leg to GND. Do not use a passive buzzer without a PWM driver circuit.

Complete Compilable Code (Register-Level I2C)

This code targets the Arduino Nano v3 (ATmega328P). It bypasses the Adafruit unified sensor library to read the raw I2C registers directly. This reduces memory overhead and exposes the exact configuration bytes sent to the sensor. The code calculates the vector magnitude of the XYZ axes in milli-g (mg) and triggers the buzzer if the vibration exceeds a 200 mg threshold.

#include <Wire.h>

// --- Pin Definitions ---
#define PIN_BUZZER 8
#define PIN_LED 13

// --- ADXL345 I2C Configuration ---
#define ADXL345_ADDRESS 0x53 // SDO tied to GND
#define REG_DEVID 0x00
#define REG_POWER_CTL 0x2D
#define REG_DATA_FORMAT 0x31
#define REG_DATAX0 0x32

// --- Thresholds & Constants ---
// Scale factor: 3.9 mg/LSB in full resolution mode
#define SCALE_FACTOR 3.9 
#define VIBRATION_THRESHOLD_MG 200.0 // Trigger at 200 milli-g
#define BASELINE_G 1000.0 // 1G resting baseline (approx)

float baselineMagnitude = 0;

void setup() {
  Serial.begin(115200);
  pinMode(PIN_BUZZER, OUTPUT);
  pinMode(PIN_LED, OUTPUT);
  
  Wire.begin();
  Wire.setClock(400000); // Set I2C to Fast Mode (400kHz)

  // 1. Verify Device ID
  Wire.beginTransmission(ADXL345_ADDRESS);
  Wire.write(REG_DEVID);
  Wire.endTransmission();
  Wire.requestFrom(ADXL345_ADDRESS, 1);
  
  if (Wire.available()) {
    uint8_t devId = Wire.read();
    if (devId != 0xE5) {
      Serial.print("Error: Unexpected DEVID 0x");
      Serial.println(devId, HEX);
      Serial.println("Expected 0xE5. Check I2C wiring and pull-ups.");
      while(1); // Halt execution
    }
  } else {
    Serial.println("Error: ADXL345 not found on I2C bus. Check connections.");
    while(1);
  }

  // 2. Configure Data Format: Full resolution, +/- 16g
  writeRegister(REG_DATA_FORMAT, 0x0B); 
  
  // 3. Configure Power: Enable measurement mode
  writeRegister(REG_POWER_CTL, 0x08); 
  
  // 4. Calibrate Baseline
  delay(100); // Let sensor settle
  calibrateBaseline();
  
  Serial.println("ADXL345 Vibration Detector Initialized.");
}

void loop() {
  // Read 6 bytes (X0, X1, Y0, Y1, Z0, Z1)
  Wire.beginTransmission(ADXL345_ADDRESS);
  Wire.write(REG_DATAX0);
  Wire.endTransmission();
  Wire.requestFrom(ADXL345_ADDRESS, 6);

  if (Wire.available() == 6) {
    int16_t rawX = Wire.read() | (Wire.read() << 8);
    int16_t rawY = Wire.read() | (Wire.read() << 8);
    int16_t rawZ = Wire.read() | (Wire.read() << 8);

    // Convert raw LSB to milli-g (mg)
    float mgX = rawX * SCALE_FACTOR;
    float mgY = rawY * SCALE_FACTOR;
    float mgZ = rawZ * SCALE_FACTOR;

    // Calculate total vector magnitude
    float magnitude = sqrt(mgX*mgX + mgY*mgY + mgZ*mgZ);
    
    // Calculate deviation from the 1G resting baseline
    float vibrationDelta = abs(magnitude - baselineMagnitude);

    if (vibrationDelta > VIBRATION_THRESHOLD_MG) {
      triggerAlarm(vibrationDelta);
    } else {
      digitalWrite(PIN_BUZZER, LOW);
      digitalWrite(PIN_LED, LOW);
    }
    
    // Optional: Print to serial plotter for debugging
    // Serial.println(vibrationDelta);
  }
  
  delay(20); // 50Hz sampling rate
}

void writeRegister(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(ADXL345_ADDRESS);
  Wire.write(reg);
  Wire.write(value);
  uint8_t error = Wire.endTransmission();
  if (error != 0) {
    Serial.print("I2C Write Error: ");
    Serial.println(error);
  }
}

void calibrateBaseline() {
  float sum = 0;
  for (int i = 0; i < 50; i++) {
    Wire.beginTransmission(ADXL345_ADDRESS);
    Wire.write(REG_DATAX0);
    Wire.endTransmission();
    Wire.requestFrom(ADXL345_ADDRESS, 6);
    if (Wire.available() == 6) {
      int16_t x = Wire.read() | (Wire.read() << 8);
      int16_t y = Wire.read() | (Wire.read() << 8);
      int16_t z = Wire.read() | (Wire.read() << 8);
      sum += sqrt(pow(x*SCALE_FACTOR, 2) + pow(y*SCALE_FACTOR, 2) + pow(z*SCALE_FACTOR, 2));
    }
    delay(10);
  }
  baselineMagnitude = sum / 50.0;
  Serial.print("Baseline Magnitude: ");
  Serial.print(baselineMagnitude);
  Serial.println(" mg");
}

void triggerAlarm(float delta) {
  digitalWrite(PIN_BUZZER, HIGH);
  digitalWrite(PIN_LED, HIGH);
  Serial.print("Vibration Detected! Delta: ");
  Serial.print(delta);
  Serial.println(" mg");
}

Debugging: First Three Things to Check When It Fails

When working with raw I2C registers, compilation errors are rare, but hardware and state-machine failures are common. If the serial monitor throws an error or the sensor behaves erratically, check these three items in order:

  1. Error String: Error: ADXL345 not found on I2C bus.
    Cause: Missing pull-up resistors or incorrect logic levels. The Wire.endTransmission() function returns a 2 (NACK on address) if the I2C lines are floating. Verify your 4.7kΩ pull-ups are installed on the 3.3V side of the level shifter, not the 5V side. Run an I2C scanner sketch to confirm the sensor responds at 0x53.
  2. Symptom: Readings stuck at 0 mg or Error: Unexpected DEVID
    Cause: The sensor is in Standby mode or the I2C address is wrong. The ADXL345 ships in standby to save power. If the POWER_CTL register (0x2D) isn't written with 0x08 (Measure bit), it will return zeros. Additionally, if the SDO pin is accidentally pulled high by stray solder flux, the address shifts to 0x1D, causing a DEVID read failure at 0x53.
  3. Symptom: False triggers / Buzzer won't stop
    Cause: USB power noise or mechanical coupling. If your Arduino Nano is powered via a cheap laptop USB port, the 5V rail can have 50-100mV of ripple, which couples into the I2C lines and corrupts the Z-axis LSB. Furthermore, if the buzzer is mounted on the same breadboard as the sensor, the acoustic vibration of the buzzer itself will feed back into the ADXL345, creating an infinite trigger loop. Move the buzzer off the sensor's breadboard.

Extending and Simplifying the Build

Depending on your end goal, you may not need register-level I2C polling, or you might need vastly more computational power.

How to Simplify the Build

If you are building a simple "drop alarm" or a knock-activated secret drawer and do not care about the exact G-force magnitude, swap the ADXL345 for an SW-420 module. Wire the SW-420 VCC to 5V, GND to GND, and the Digital Out to Nano D2. Use the attachInterrupt() function on D2 to trigger the buzzer. This eliminates the I2C bus, the level shifter, and the vector math entirely, reducing the BOM cost to under $2.00 and the code to 20 lines.

How to Extend the Build

For industrial predictive maintenance (e.g., monitoring a CNC spindle or lathe motor for bearing wear), the Arduino Nano's 2KB SRAM is a bottleneck. Upgrade to an ESP32-S3 DevKitC-1. The ESP32 allows you to:

  • Sample at 3200Hz using the ADXL345's 32-sample FIFO buffer and the INT1 hardware interrupt pin.
  • Perform Fast Fourier Transform (FFT) analysis on the ESP32's dual-core processor to isolate specific vibration frequencies (e.g., a 60Hz motor hum vs. a 2kHz bearing defect).
  • Push the frequency-domain data via MQTT over WiFi to a Home Assistant dashboard or an InfluxDB time-series database for long-term trend analysis.
Safety Note: When attaching sensors to industrial mains-powered machinery, ensure the sensor breakout board is galvanically isolated or housed in a Class II insulated enclosure. A fault in the machinery's grounding can push lethal line voltage back through the sensor's I2C ground line into your microcontroller and laptop.