You dont have javascript enabled! Please enable it!

Practical case: IMU vibration issues on Raspberry Pi 4

Practical case: IMU vibration issues on Raspberry Pi 4 — hero

Objective and use case

What you’ll build: A real-time vibration anomaly detector using a Raspberry Pi 4 and Pimoroni ICM-20948 IMU to collect and analyze vibration data.

Why it matters / Use cases

  • Monitor industrial machinery to detect early signs of mechanical failure, reducing downtime and maintenance costs.
  • Implement in smart home systems to ensure structural integrity by detecting abnormal vibrations in buildings.
  • Use in automotive applications to analyze vibrations in vehicles for improved safety and performance.
  • Integrate with IoT systems to provide real-time alerts for equipment anomalies in remote locations.

Expected outcome

  • Detect and flag abnormal vibration patterns with a latency of less than 1 second.
  • Achieve over 90% accuracy in anomaly detection based on frequency-domain features.
  • Process data at a rate of 100 packets/s from the ICM-20948 IMU.
  • Generate alerts for anomalies with a clear message indicating the type of vibration detected.

Audience: Intermediate developers; Level: Advanced

Architecture/flow: Data collection from ICM-20948 IMU → Signal processing → Anomaly detection model → Real-time alerting

Raspberry Pi 4 Model B + Pimoroni ICM-20948 9DoF IMU: Vibration Anomaly Detection (Advanced)

This hands-on project builds a complete real-time vibration anomaly detector using a Raspberry Pi 4 Model B running Raspberry Pi OS Bookworm 64-bit (Python 3.11) and a Pimoroni ICM-20948 9DoF IMU. You will collect IMU data, compute frequency-domain features, train an unsupervised anomaly model, and run live detection to flag abnormal vibration patterns.

The guide assumes you are comfortable with Linux shell, Python, and basic signal processing. No circuit drawings are used; all connections, commands, and code are precisely described in text, tables, and blocks.


Prerequisites

  • Raspberry Pi OS Bookworm 64-bit installed on your Raspberry Pi 4 Model B.
  • Internet access on the Pi.
  • Terminal access (local or SSH).
  • Basic knowledge of Python virtual environments and command-line usage.

OS and Python defaults:
– Raspberry Pi OS Bookworm (64-bit)
– Python 3.11 (default python3 on Bookworm)


Materials (exact models)

  • Raspberry Pi 4 Model B (any RAM size).
  • Pimoroni ICM-20948 9DoF IMU breakout (I2C mode).
  • Jumper wires (female-female if using Pi header and breakout).
  • Optional: Qwiic/STEMMA QT cable if your Pimoroni board supports JST-SH connectors.
  • Stable 5V/3A USB-C power supply for the Raspberry Pi 4 Model B.
  • A vibrating source for validation (e.g., a small DC motor, an electric toothbrush, or a vibration motor).
  • Mounting tape or zip ties to secure the IMU to the vibrating object.

Setup/Connection

1) Enable I2C interface

You can enable I2C either interactively or by editing the config file.

Option A: raspi-config (non-interactive or interactive)
– Interactive:
1. Run: sudo raspi-config
2. Interface Options → I2C → Enable.
3. Finish and reboot.

  • Non-interactive:
sudo raspi-config nonint do_i2c 0
sudo reboot

Option B: Edit config file
– Add/ensure the following lines exist:

sudoedit /boot/firmware/config.txt

Append:

dtparam=i2c_arm=on
dtparam=i2c_arm_baudrate=400000

Then:

sudo reboot

After reboot, add your user to the i2c group:

sudo adduser $USER i2c
newgrp i2c

Verify the I2C bus:

ls -l /dev/i2c-1

2) Wiring the Pimoroni ICM-20948 to the Pi (I2C)

Use 3.3V logic only. The ICM-20948 default I2C address is 0x68 (0x69 if the address pad is configured accordingly on the breakout).

Connection map:

Raspberry Pi 4 Model B Pin Signal IMU Pin (Pimoroni ICM-20948)
Physical pin 1 3V3 3V3
Physical pin 6 GND GND
Physical pin 3 (GPIO2) SDA1 SDA
Physical pin 5 (GPIO3) SCL1 SCL
  • Keep wires short and secure.
  • Ensure common ground between Pi and IMU.
  • If your Pimoroni board has a Qwiic connector, you may use a Qwiic cable to the Pi via a Qwiic-to-Pi shim; otherwise, wire as above.

Check device detection:

sudo apt update
sudo apt install -y i2c-tools
i2cdetect -y 1

You should see “68” (or “69”) in the output grid.


Full Code

Below is a single-file, advanced script implementing:
– A minimal ICM-20948 I2C driver (accelerometer + gyroscope)
– Baseline collection
– Feature extraction (time and frequency domain)
– Model training (Isolation Forest + StandardScaler)
– Real-time anomaly detection

Save as:
– /home/pi/projects/imu-vibe-anomaly/imu_vibe.py

Create the directory first:

mkdir -p /home/pi/projects/imu-vibe-anomaly/data

Then create imu_vibe.py with the content below:

#!/usr/bin/env python3
# Python 3.11, Raspberry Pi OS Bookworm 64-bit

import os
import sys
import time
import math
import json
import argparse
import struct
import csv
from dataclasses import dataclass
from typing import Tuple, List

import numpy as np
from smbus2 import SMBus, i2c_msg

# Optional heavy imports are gated by mode
# pip: numpy==1.26.4 scipy==1.11.4 scikit-learn==1.3.2 joblib==1.3.2
try:
    from scipy.signal import welch
    from sklearn.ensemble import IsolationForest
    from sklearn.preprocessing import StandardScaler
    import joblib
except Exception:
    # In modes that don't need ML, we ignore; validate before run
    pass

# ICM-20948 register map (subset) and configuration constants
ICM20948_I2C_ADDR = 0x68  # default; 0x69 if AD0/ADDR pad pulled

# Common registers (Bank 0)
REG_BANK_SEL      = 0x7F  # bank select register
BANK0             = 0x00
BANK2             = 0x20

WHO_AM_I          = 0x00  # should read 0xEA
USER_CTRL         = 0x03
LP_CONFIG         = 0x05
PWR_MGMT_1        = 0x06
PWR_MGMT_2        = 0x07
INT_PIN_CFG       = 0x0F

# Output registers (Bank 0)
ACCEL_XOUT_H      = 0x2D
ACCEL_XOUT_LEN    = 6
GYRO_XOUT_H       = 0x33
GYRO_XOUT_LEN     = 6

# Bank 2 registers for configuration
GYRO_SMPLRT_DIV   = 0x00
GYRO_CONFIG_1     = 0x01
# GYRO_CONFIG_2   = 0x02  # not used here

ACCEL_SMPLRT_DIV_1 = 0x10
ACCEL_SMPLRT_DIV_2 = 0x11
ACCEL_CONFIG       = 0x14
# ACCEL_CONFIG_2   = 0x15  # not used here

WHO_AM_I_EXPECTED = 0xEA

@dataclass
class IMUScales:
    accel_fs_g: int          # ±g range
    gyro_fs_dps: int         # ±dps range
    accel_lsb_per_g: float   # counts per g
    gyro_lsb_per_dps: float  # counts per dps

class ICM20948:
    """
    Minimal I2C driver for Pimoroni ICM-20948 (Accel + Gyro).
    Magnetometer not used in this project.
    """
    def __init__(self, i2c_bus: int = 1, addr: int = ICM20948_I2C_ADDR, i2c_baud: int = 400000):
        self.addr = addr
        self.bus = SMBus(i2c_bus)
        # Scale defaults (will be set during configure)
        self.scales = IMUScales(accel_fs_g=16, gyro_fs_dps=2000,
                                accel_lsb_per_g=2048.0, gyro_lsb_per_dps=16.4)

    def _select_bank(self, bank: int):
        self._write_u8(REG_BANK_SEL, bank & 0x30)  # valid bank bits: [5:4]

    def _write_u8(self, reg: int, val: int):
        self.bus.write_byte_data(self.addr, reg, val & 0xFF)

    def _read_u8(self, reg: int) -> int:
        return self.bus.read_byte_data(self.addr, reg)

    def _read_block(self, reg: int, length: int) -> bytes:
        read = i2c_msg.read(self.addr, length)
        self.bus.i2c_rdwr(i2c_msg.write(self.addr, [reg]), read)
        return bytes(read)

    def who_am_i(self) -> int:
        self._select_bank(BANK0)
        return self._read_u8(WHO_AM_I)

    def reset_and_wake(self):
        self._select_bank(BANK0)
        # Clear sleep, select auto clock (bit0=1), others default
        self._write_u8(PWR_MGMT_1, 0x01)
        time.sleep(0.05)
        # Enable accel + gyro (clear disables)
        self._write_u8(PWR_MGMT_2, 0x00)
        time.sleep(0.01)
        # Optional: route INT pin, bypass not needed for this project
        self._write_u8(INT_PIN_CFG, 0x00)

    def configure(self, accel_fs_g: int = 16, gyro_fs_dps: int = 2000,
                  dlpf_accel: int = 3, dlpf_gyro: int = 3,
                  sample_rate_hz: int = 200):
        """
        Configure full-scale ranges, DLPF, and sample rate.
        dlpf_*: 0..7 (lower => more filtering). 3 is a reasonable default.
        sample_rate_hz: target sample rate for both accel and gyro.
        """
        self._select_bank(BANK2)

        # Gyro full-scale config (GYRO_CONFIG_1)
        # FS_SEL bits [2:1]: 00=250, 01=500, 10=1000, 11=2000 dps
        gyro_fs_bits = {250:0, 500:1, 1000:2, 2000:3}[gyro_fs_dps] << 1
        gyro_dlpfcfg = (dlpf_gyro & 0x07)
        gyro_cfg1 = gyro_fs_bits | gyro_dlpfcfg
        self._write_u8(GYRO_CONFIG_1, gyro_cfg1)

        # Gyro sample rate: f_sample = 1100 / (1 + GYRO_SMPLRT_DIV) for DLPF enabled
        # Use an integer divider to approximate sample_rate_hz
        # We want ~200 Hz by default
        gyro_div = max(0, int(round(1100 / max(1, sample_rate_hz) - 1)))
        self._write_u8(GYRO_SMPLRT_DIV, gyro_div)

        # Accel full-scale config (ACCEL_CONFIG)
        # FS_SEL bits [2:1]: 00=2g, 01=4g, 10=8g, 11=16g
        accel_fs_bits = {2:0, 4:1, 8:2, 16:3}[accel_fs_g] << 1
        accel_dlpfcfg = (dlpf_accel & 0x07)
        accel_cfg = accel_fs_bits | accel_dlpfcfg
        self._write_u8(ACCEL_CONFIG, accel_cfg)

        # Accel sample rate: f_sample = 1125 / (1 + ACCEL_SMPLRT_DIV)
        # Div is 12-bit across two regs: DIV_1 (upper), DIV_2 (lower)
        accel_div = max(0, int(round(1125 / max(1, sample_rate_hz) - 1)))
        self._write_u8(ACCEL_SMPLRT_DIV_1, (accel_div >> 8) & 0x0F)
        self._write_u8(ACCEL_SMPLRT_DIV_2, accel_div & 0xFF)

        # Update conversion scales based on selected full-scales
        accel_lsb_per_g_map = {2:16384.0, 4:8192.0, 8:4096.0, 16:2048.0}
        gyro_lsb_per_dps_map = {250:131.0, 500:65.5, 1000:32.8, 2000:16.4}
        self.scales = IMUScales(
            accel_fs_g=accel_fs_g,
            gyro_fs_dps=gyro_fs_dps,
            accel_lsb_per_g=accel_lsb_per_g_map[accel_fs_g],
            gyro_lsb_per_dps=gyro_lsb_per_dps_map[gyro_fs_dps],
        )

        self._select_bank(BANK0)

    def read_accel_gyro(self) -> Tuple[Tuple[float,float,float], Tuple[float,float,float]]:
        """
        Read accelerometer and gyroscope; return values in g and dps.
        """
        # Read accel (6 bytes) then gyro (6 bytes)
        self._select_bank(BANK0)
        accel_bytes = self._read_block(ACCEL_XOUT_H, ACCEL_XOUT_LEN)
        gyro_bytes  = self._read_block(GYRO_XOUT_H, GYRO_XOUT_LEN)

        ax, ay, az = struct.unpack(">hhh", accel_bytes)
        gx, gy, gz = struct.unpack(">hhh", gyro_bytes)

        ax_g = ax / self.scales.accel_lsb_per_g
        ay_g = ay / self.scales.accel_lsb_per_g
        az_g = az / self.scales.accel_lsb_per_g

        gx_dps = gx / self.scales.gyro_lsb_per_dps
        gy_dps = gy / self.scales.gyro_lsb_per_dps
        gz_dps = gz / self.scales.gyro_lsb_per_dps

        return (ax_g, ay_g, az_g), (gx_dps, gy_dps, gz_dps)

def magnitude3(v: Tuple[float,float,float]) -> float:
    return math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])

def extract_features(accel_window: np.ndarray, gyro_window: np.ndarray, fs: float) -> np.ndarray:
    """
    Compute a compact feature vector capturing vibration signatures.
    Inputs are Nx3 arrays (columns: x,y,z).
    """
    # Magnitudes to reduce orientation dependency
    a_mag = np.linalg.norm(accel_window, axis=1)
    g_mag = np.linalg.norm(gyro_window, axis=1)

    # Time-domain stats
    def stats(x):
        return np.array([
            np.mean(x), np.std(x), np.sqrt(np.mean(x*x)),  # mean, std, RMS
            np.max(x) - np.min(x),                        # peak-to-peak
            np.median(x),
            np.percentile(x, 95) - np.percentile(x, 5),   # robust range
        ], dtype=np.float64)

    a_stats = stats(a_mag)
    g_stats = stats(g_mag)

    # Frequency-domain (Welch PSD on accel magnitude)
    f, Pxx = welch(a_mag, fs=fs, nperseg=min(256, len(a_mag)))
    Pxx = Pxx + 1e-16  # avoid log/0
    # Band powers
    def bandpower(f, Pxx, fmin, fmax):
        idx = np.logical_and(f >= fmin, f < fmax)
        return np.trapz(Pxx[idx], f[idx]) if np.any(idx) else 0.0

    bp_0_10   = bandpower(f, Pxx, 0.0, 10.0)
    bp_10_50  = bandpower(f, Pxx, 10.0, 50.0)
    bp_50_100 = bandpower(f, Pxx, 50.0, 100.0)

    # Spectral centroid and dominant frequency
    centroid = np.sum(f * Pxx) / np.sum(Pxx)
    dom_idx = np.argmax(Pxx)
    dom_freq = f[dom_idx]
    dom_amp = Pxx[dom_idx]

    features = np.concatenate([
        a_stats, g_stats,
        np.array([bp_0_10, bp_10_50, bp_50_100, centroid, dom_freq, dom_amp])
    ])
    return features

def collect_samples(imu: ICM20948, fs: float, seconds: float, bias_cal: dict | None) -> Tuple[np.ndarray, np.ndarray]:
    """
    Collect accel/gyro samples for `seconds` at rate `fs`.
    Returns arrays shape (N,3) for accel and gyro.
    Applies optional bias calibration.
    """
    dt = 1.0 / fs
    N = int(seconds * fs)
    accel = np.zeros((N, 3), dtype=np.float64)
    gyro = np.zeros((N, 3), dtype=np.float64)

    next_t = time.perf_counter()
    for i in range(N):
        a, g = imu.read_accel_gyro()
        ax, ay, az = a
        gx, gy, gz = g

        if bias_cal:
            ax -= bias_cal.get("accel_bias_x", 0.0)
            ay -= bias_cal.get("accel_bias_y", 0.0)
            az -= bias_cal.get("accel_bias_z", 0.0)
            gx -= bias_cal.get("gyro_bias_x", 0.0)
            gy -= bias_cal.get("gyro_bias_y", 0.0)
            gz -= bias_cal.get("gyro_bias_z", 0.0)

        accel[i, :] = (ax, ay, az)
        gyro[i, :] = (gx, gy, gz)

        next_t += dt
        # Sleep to maintain rate; compensate for read time
        rem = next_t - time.perf_counter()
        if rem > 0:
            time.sleep(rem)
    return accel, gyro

def calibrate_bias(imu: ICM20948, fs: float = 200.0, seconds: float = 5.0) -> dict:
    """
    Estimate simple biases by averaging samples while device is stationary.
    Note: accelerometer includes gravity; we compute mean per axis so magnitude-based features are less affected.
    """
    accel, gyro = collect_samples(imu, fs=fs, seconds=seconds, bias_cal=None)
    a_bias = np.mean(accel, axis=0)
    g_bias = np.mean(gyro, axis=0)
    return {
        "accel_bias_x": float(a_bias[0]),
        "accel_bias_y": float(a_bias[1]),
        "accel_bias_z": float(a_bias[2]),
        "gyro_bias_x": float(g_bias[0]),
        "gyro_bias_y": float(g_bias[1]),
        "gyro_bias_z": float(g_bias[2]),
        "fs": fs
    }

def sliding_windows(data_a: np.ndarray, data_g: np.ndarray, win: int, step: int):
    """
    Generator of overlapping windows.
    """
    N = data_a.shape[0]
    for start in range(0, N - win + 1, step):
        end = start + win
        yield data_a[start:end, :], data_g[start:end, :]

def save_csv_features(path: str, features: List[np.ndarray]):
    with open(path, "w", newline="") as f:
        w = csv.writer(f)
        for feat in features:
            w.writerow([float(x) for x in feat])

def load_csv_features(path: str) -> np.ndarray:
    rows = []
    with open(path, "r", newline="") as f:
        r = csv.reader(f)
        for row in r:
            rows.append([float(x) for x in row])
    return np.array(rows, dtype=np.float64)

def cmd_whoami(args):
    imu = ICM20948(i2c_bus=1, addr=args.addr)
    ident = imu.who_am_i()
    print(f"ICM-20948 WHO_AM_I: 0x{ident:02X}")
    if ident != WHO_AM_I_EXPECTED:
        print("WARNING: Unexpected WHO_AM_I. Check address and wiring.", file=sys.stderr)

def cmd_collect_baseline(args):
    imu = ICM20948(i2c_bus=1, addr=args.addr)
    ident = imu.who_am_i()
    if ident != WHO_AM_I_EXPECTED:
        print(f"WHO_AM_I mismatch: 0x{ident:02X} != 0x{WHO_AM_I_EXPECTED:02X}", file=sys.stderr)
    imu.reset_and_wake()
    imu.configure(accel_fs_g=16, gyro_fs_dps=2000, dlpf_accel=3, dlpf_gyro=3, sample_rate_hz=args.fs)

    print("Calibrating biases... keep IMU still")
    bias = calibrate_bias(imu, fs=args.fs, seconds=5.0)
    print("Bias:", bias)
    os.makedirs(args.outdir, exist_ok=True)
    with open(os.path.join(args.outdir, "bias.json"), "w") as f:
        json.dump(bias, f, indent=2)

    print(f"Collecting baseline for {args.seconds:.1f}s at {args.fs:.1f} Hz...")
    accel, gyro = collect_samples(imu, fs=args.fs, seconds=args.seconds, bias_cal=bias)

    # Windowing
    win = int(args.fs * args.win_s)
    step = int(win // 2)  # 50% overlap
    feats = []
    for a_w, g_w in sliding_windows(accel, gyro, win, step):
        feats.append(extract_features(a_w, g_w, fs=args.fs))
    feat_path = os.path.join(args.outdir, "baseline_features.csv")
    save_csv_features(feat_path, feats)
    print(f"Saved baseline features: {feat_path}, windows: {len(feats)}")

def cmd_train(args):
    feat_path = os.path.join(args.outdir, "baseline_features.csv")
    X = load_csv_features(feat_path)
    print(f"Loaded baseline features: {X.shape}")

    scaler = StandardScaler()
    Xs = scaler.fit_transform(X)

    # Unsupervised anomaly detector
    model = IsolationForest(
        n_estimators=200,
        contamination=args.contamination,
        random_state=42,
        n_jobs=-1
    )
    model.fit(Xs)

    joblib.dump(scaler, os.path.join(args.outdir, "scaler.pkl"))
    joblib.dump(model, os.path.join(args.outdir, "iforest.pkl"))
    print(f"Saved scaler and model in {args.outdir}")

def cmd_detect(args):
    imu = ICM20948(i2c_bus=1, addr=args.addr)
    imu.reset_and_wake()
    imu.configure(accel_fs_g=16, gyro_fs_dps=2000, dlpf_accel=3, dlpf_gyro=3, sample_rate_hz=args.fs)

    with open(os.path.join(args.outdir, "bias.json"), "r") as f:
        bias = json.load(f)

    scaler = joblib.load(os.path.join(args.outdir, "scaler.pkl"))
    model = joblib.load(os.path.join(args.outdir, "iforest.pkl"))

    win = int(args.fs * args.win_s)
    step = int(win // 2)

    print("Starting live detection. Press Ctrl+C to stop.")
    buf_a = np.zeros((0, 3), dtype=np.float64)
    buf_g = np.zeros((0, 3), dtype=np.float64)

    try:
        while True:
            a, g = collect_samples(imu, fs=args.fs, seconds=args.win_s/2, bias_cal=bias)  # half-window chunk
            buf_a = np.vstack([buf_a, a])
            buf_g = np.vstack([buf_g, g])

            # Slide windows across the buffer
            idx = 0
            while idx + win <= buf_a.shape[0]:
                a_w = buf_a[idx:idx+win, :]
                g_w = buf_g[idx:idx+win, :]
                feat = extract_features(a_w, g_w, fs=args.fs).reshape(1, -1)
                feat_s = scaler.transform(feat)
                pred = model.predict(feat_s)[0]  # 1=normal, -1=anomaly
                score = model.score_samples(feat_s)[0]  # higher is more normal
                status = "ANOMALY" if pred == -1 else "normal "
                tstamp = time.strftime("%Y-%m-%d %H:%M:%S")
                print(f"{tstamp}  {status}  score={score:.3f}")
                idx += step

            # Keep only the last (win - step) samples to maintain overlap
            if buf_a.shape[0] > win:
                buf_a = buf_a[-(win - step):, :]
                buf_g = buf_g[-(win - step):, :]
    except KeyboardInterrupt:
        print("\nStopped.")

def cmd_configure_only(args):
    imu = ICM20948(i2c_bus=1, addr=args.addr)
    imu.reset_and_wake()
    imu.configure(accel_fs_g=16, gyro_fs_dps=2000, dlpf_accel=3, dlpf_gyro=3, sample_rate_hz=args.fs)
    print("Configured IMU at ~{} Hz, FS: ±{}g / ±{} dps".format(args.fs, 16, 2000))

def main():
    parser = argparse.ArgumentParser(description="ICM-20948 Vibration Anomaly Detection")
    parser.add_argument("--addr", type=lambda x: int(x, 0), default=0x68, help="I2C address (0x68 or 0x69)")
    parser.add_argument("--fs", type=float, default=200.0, help="Target sample rate (Hz, default 200)")
    parser.add_argument("--outdir", type=str, default="/home/pi/projects/imu-vibe-anomaly/data")
    parser.add_argument("--win-s", type=float, default=2.0, help="Window size in seconds (default 2.0)")

    sub = parser.add_subparsers(dest="mode", required=True)
    sub.add_parser("whoami")
    sub.add_parser("configure")

    p_collect = sub.add_parser("collect-baseline")
    p_collect.add_argument("--seconds", type=float, default=60.0, help="Baseline collection duration in seconds")

    p_train = sub.add_parser("train")
    p_train.add_argument("--contamination", type=float, default=0.05, help="Expected anomaly fraction (0..0.5)")

    sub.add_parser("detect")

    args = parser.parse_args()

    if args.mode == "whoami":
        cmd_whoami(args)
    elif args.mode == "configure":
        cmd_configure_only(args)
    elif args.mode == "collect-baseline":
        cmd_collect_baseline(args)
    elif args.mode == "train":
        cmd_train(args)
    elif args.mode == "detect":
        cmd_detect(args)
    else:
        print("Unknown mode")
        sys.exit(1)

if __name__ == "__main__":
    main()

Notes:
– The driver configures accelerometer and gyro to ±16g and ±2000 dps with DLPF=3 and a target sample rate of ~200 Hz.
– WHO_AM_I expected value for ICM-20948 is 0xEA.
– For demanding vibration capture, consider using the sensor FIFO or SPI. Here we target 200 Hz with I2C at 400 kHz for simplicity.


Build/Flash/Run commands

There is nothing to “flash,” but you will set up a Python 3.11 virtual environment, install dependencies, and run the script.

1) System packages and Python venv

sudo apt update
sudo apt install -y python3-venv python3-dev python3-pip i2c-tools git libatlas-base-dev

Create and activate a virtual environment:

python3 -m venv /home/pi/venvs/imu-venv
source /home/pi/venvs/imu-venv/bin/activate
python -V    # Should show Python 3.11.x
pip -V       # pip for the venv

Upgrade pip and install Python packages (pin versions for reproducibility):

pip install --upgrade pip
pip install numpy==1.26.4 scipy==1.11.4 scikit-learn==1.3.2 joblib==1.3.2 smbus2==0.4.3 matplotlib==3.8.2

Project directory and script:

mkdir -p /home/pi/projects/imu-vibe-anomaly/data
nano /home/pi/projects/imu-vibe-anomaly/imu_vibe.py
# Paste the Full Code above, save with Ctrl+O, Enter, exit with Ctrl+X
chmod +x /home/pi/projects/imu-vibe-anomaly/imu_vibe.py

2) Validate I2C presence and WHO_AM_I

i2cdetect -y 1
# Expect to see 68 (or 69)
python /home/pi/projects/imu-vibe-anomaly/imu_vibe.py --addr 0x68 whoami

If your board is configured as 0x69, pass --addr 0x69.

3) Configure the IMU (sanity check)

python /home/pi/projects/imu-vibe-anomaly/imu_vibe.py --fs 200 configure

This ensures the IMU is awake and set to desired ranges.

4) Collect baseline features (normal vibration)

Mount the IMU on your target machine in a known “normal” state. The baseline should represent healthy operation.

python /home/pi/projects/imu-vibe-anomaly/imu_vibe.py --fs 200 --win-s 2.0 --outdir /home/pi/projects/imu-vibe-anomaly/data collect-baseline --seconds 60

Generated files:
– /home/pi/projects/imu-vibe-anomaly/data/bias.json
– /home/pi/projects/imu-vibe-anomaly/data/baseline_features.csv

5) Train the anomaly model

python /home/pi/projects/imu-vibe-anomaly/imu_vibe.py --outdir /home/pi/projects/imu-vibe-anomaly/data train --contamination 0.05

Artifacts:
– scaler.pkl
– iforest.pkl

6) Run real-time anomaly detection

Attach the IMU to the machine. Intentionally induce an abnormal vibration (e.g., add a small imbalance or press an object lightly onto the rotating part) to see detection transitions.

python /home/pi/projects/imu-vibe-anomaly/imu_vibe.py --fs 200 --win-s 2.0 --outdir /home/pi/projects/imu-vibe-anomaly/data detect

Observe printed lines with timestamps, “normal” or “ANOMALY,” and a score.


Step-by-step Validation

1) Hardware wiring check
– Confirm connections match the table (3V3, GND, SDA, SCL).
– Ensure solid mechanical mounting to capture realistic vibrations (tape or zip ties).

2) I2C device enumerates
– Run:
i2cdetect -y 1
Expect 68 (or 69) in the map.

3) WHO_AM_I matches
– Run:
python /home/pi/projects/imu-vibe-anomaly/imu_vibe.py --addr 0x68 whoami
Output should include “ICM-20948 WHO_AM_I: 0xEA”.

4) IMU configuration applies
– Run:
python /home/pi/projects/imu-vibe-anomaly/imu_vibe.py --fs 200 configure
You should see a confirmation line of configuration.

5) Bias calibration while stationary
– During baseline collection, keep the IMU still for the first 5 seconds while it calibrates biases. The command prints mean biases; values are typically small (gyro near 0 dps; accelerometer around gravity on one axis).

6) Baseline capture (healthy)
– With the machine in a healthy state, run the baseline collection for 60 seconds (or more for richer data). The script computes overlapping window features and writes baseline_features.csv. Check the number of windows reported; for fs=200 Hz, win=2.0 s, step=1.0 s, ~59 windows for 60 s.

7) Model training
– After training, confirm scaler.pkl and iforest.pkl exist:
ls -lh /home/pi/projects/imu-vibe-anomaly/data/*.pkl

8) Passive detection (no anomaly)
– Run detect mode without introducing anomalies. Expect mostly “normal” with stable scores. If many “ANOMALY” lines appear, expand baseline time or reduce contamination to 0.02.

9) Active anomaly test
– Introduce a mechanical disturbance:
– Add a small weight to a rotor.
– Touch the housing to change stiffness/coupling.
– Swap to a rougher surface.
– Observe “ANOMALY” lines; the score should be lower than normal windows.

10) Persistence and transition
– Remove the disturbance. The detector should return to “normal” within a couple of windows (1–2 seconds if win=2.0 s with 50% overlap).


Troubleshooting

  • I2C device not found (i2cdetect shows “–” where 0x68/0x69 should be):
  • Ensure I2C is enabled in /boot/firmware/config.txt:
    • dtparam=i2c_arm=on
    • dtparam=i2c_arm_baudrate=400000
  • Check wiring: SDA to GPIO2 (pin 3), SCL to GPIO3 (pin 5), 3V3, and GND.
  • Some Pimoroni boards default to 0x69; try:
    i2cdetect -y 1
    python imu_vibe.py --addr 0x69 whoami

  • Permission denied on /dev/i2c-1:

  • Add your user to the i2c group:
    sudo adduser $USER i2c
    newgrp i2c

  • WHO_AM_I not 0xEA:

  • Address incorrect (try 0x69).
  • Board variant or wiring error. Double-check supply and connections.
  • If still failing, slow I2C to 100 kHz temporarily by setting:
    dtparam=i2c_arm_baudrate=100000
    in /boot/firmware/config.txt, then reboot.

  • Unstable sampling interval or missed real-time deadlines:

  • Reduce sample rate (e.g., –fs 150).
  • Close other heavy processes (disable desktop/GUI if running).
  • Ensure proper power; undervoltage causes throttling. Check:
    vcgencmd get_throttled
    Value should be 0x0 for no throttling.

  • Too many false positives:

  • Increase baseline duration and variability (collect in multiple healthy operating conditions).
  • Reduce contamination (e.g., 0.02).
  • Increase window size (–win-s 3.0) for more stable spectral estimates.
  • Consider removing low-frequency drift by high-pass filtering acceleration (modify extract_features).

  • Fails to install SciPy or scikit-learn:

  • Ensure libatlas-base-dev is installed (provided in apt install line).
  • Increase swap if compiling from source (Bookworm usually provides wheels).
  • Alternatively, use simpler feature thresholds without ML as a stopgap.

  • Model not sensitive enough:

  • Increase contamination (e.g., 0.1).
  • Add higher-frequency bandpowers (extend 100–200 Hz if sampling supports it).
  • Mount IMU more tightly to improve signal coupling.

Improvements

  • Use SPI for higher throughput:
  • The ICM-20948 supports SPI. Switching from I2C to SPI with spidev can raise sampling rates and reduce latency.

  • Leverage sensor FIFO:

  • Configure and burst-read the FIFO to reduce per-sample I2C overhead and jitter, achieving more reliable high-rate capture.

  • Advanced signal processing:

  • Envelope analysis for bearings (Hilbert transform).
  • Cepstrum analysis and spectral kurtosis for resonance detection.
  • Order tracking for variable-speed rotating machinery.

  • Machine learning enhancements:

  • Replace Isolation Forest with One-Class SVM or an autoencoder (e.g., TensorFlow Lite on the Pi).
  • Train separate models for different machine states (RPM ranges) and select dynamically.

  • Orientation compensation:

  • Remove gravity by estimating orientation (from gyro integration + complementary filter/accel) and projecting to the horizontal plane.

  • Edge deployment:

  • Package the detector as a systemd service.
  • Add MQTT publishing of anomaly events.
  • Persist logs with timestamps and scores to CSV or InfluxDB.

  • Calibration refinement:

  • Temperature compensation for gyro drift.
  • Multi-position accelerometer calibration to refine offsets and scale.

  • Robustness:

  • Use hardware timestamping via GPIO interrupts and DRDY pins for precise timing.
  • Add watchdog/retry logic on I2C transfer errors.

Final Checklist

  • Raspberry Pi OS Bookworm 64-bit is installed and up to date.
  • I2C enabled via raspi-config or /boot/firmware/config.txt; baud set to 400 kHz.
  • Wiring verified:
  • 3V3 (pin 1) to IMU 3V3
  • GND (pin 6) to IMU GND
  • SDA1 (pin 3) to IMU SDA
  • SCL1 (pin 5) to IMU SCL
  • i2cdetect shows 0x68 (or 0x69).
  • Python venv created at /home/pi/venvs/imu-venv and activated.
  • Packages installed:
  • numpy==1.26.4, scipy==1.11.4, scikit-learn==1.3.2, joblib==1.3.2, smbus2==0.4.3
  • WHO_AM_I check returns 0xEA.
  • Baseline collected (≥60 s) and stored in /home/pi/projects/imu-vibe-anomaly/data.
  • Model trained; scaler.pkl and iforest.pkl present.
  • Live detection runs, prints “normal” under healthy conditions.
  • Anomaly is detected when introducing a controlled vibration change.
  • Troubleshooting items reviewed if any step fails.
  • Considered improvements for higher fidelity or deployment.

This completes a practical, end-to-end vibration anomaly detection pipeline on Raspberry Pi 4 Model B using a Pimoroni ICM-20948 9DoF IMU with Raspberry Pi OS Bookworm and Python 3.11.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary purpose of the project described in the article?




Question 2: Which Raspberry Pi model is used in the project?




Question 3: What type of IMU is used in the project?




Question 4: Which programming language is used in this project?




Question 5: What is required to enable the I2C interface?




Question 6: What is a prerequisite for running the project?




Question 7: What type of power supply is recommended for the Raspberry Pi 4 Model B?




Question 8: Which command is suggested to run for enabling I2C interactively?




Question 9: What is the operating system used in the project?




Question 10: What is the main feature of the ICM-20948 IMU used in the project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top