← SHEET 02 · ASSEMBLIES EL-009
IMU Digital Spirit Level
| PART NO | EL-009 |
|---|---|
| MATL / SYSTEM | ESP32 · MPU6050 · SSD1306 |
| TOOLS | Arduino IDE · Wokwi · C++ |
A pocket spirit level built on the same estimation math as the balance robots elsewhere on this site: the TWIP and ballbot state estimators fuse a noisy absolute sensor with a drifting rate sensor, and this project strips that problem down to its minimum working form. An ESP32 reads an MPU6050 over raw I2C, a complementary filter (tau = 1.0 s) blends accelerometer tilt with integrated gyro rate, and an SSD1306 draws a 2-axis bubble with numeric pitch/roll to 0.1°. A button tares the zero into flash; a buzzer holds a tone when both axes sit within ±0.2° of level.
OVERVIEW & MOTIVATION
Accelerometer-only tilt meters jitter; gyro-only ones drift. Every balancing robot solves this with some flavor of sensor fusion before its controller sees an angle, and the complementary filter is the smallest honest member of that family — one pole, one parameter, closed-form. The spirit level is the ideal carrier for it: the device is read mostly at rest, so the filter can be tuned slow (heavy vibration rejection) and the whole estimator fits in two lines of C++. Everything runs at a fixed 100 Hz so the discrete filter’s dt is exact, and calibration is a real tare (accel zero + gyro bias captured together, persisted in NVS) rather than a hardcoded offset.
COMPONENTS & BOM
| REF | COMPONENT | SPEC | ROLE |
|---|---|---|---|
| U1 | ESP32 DevKit-C v4 | dual-core 240 MHz, 3.3 V I/O | runs the 100 Hz fusion loop, I2C master |
| U2 | MPU6050 module (GY-521) | 3-axis accel ±2 g + 3-axis gyro ±250 °/s, I2C 0x68 | tilt + rate sensing |
| U3 | SSD1306 OLED | 128×64, I2C 0x3C, 3.3 V | bubble display + numeric readout |
| SW1 | Pushbutton | momentary, to GND | zero-calibration tare |
| BZ1 | Passive buzzer | driven via tone(), ~2.2 kHz | level indication beep |
WIRING
| ESP32 PIN | NET | PERIPHERAL PIN |
|---|---|---|
| GPIO21 | I2C SDA | MPU6050 SDA · SSD1306 SDA |
| GPIO22 | I2C SCL | MPU6050 SCL · SSD1306 SCL |
| GPIO4 | CAL_BTN (INPUT_PULLUP, active low) | Button leg 1 (leg 2 → GND) |
| GPIO26 | BUZZER | Buzzer + |
| 3V3 | power | MPU6050 VCC · SSD1306 VCC |
| GND | ground | MPU6050 · SSD1306 · button · buzzer − |
Both I2C devices share the one bus — MPU6050 answers at 0x68 (AD0 low), SSD1306 at 0x3C, so no address conflict and no extra pins.
SENSOR FUSION
Two angle estimates exist every sample, each broken in a complementary way.
The accelerometer gives an absolute tilt (atan2 of the gravity components)
with zero drift — but it is noisy and reads wrong under any linear
acceleration: bump the surface and the “angle” jumps. The gyro gives a clean,
motion-immune rate, but integrating it accumulates bias into unbounded drift.
The complementary filter low-passes the accel angle and high-passes the
gyro-integrated angle with the same corner, so the weights sum to one at every
frequency. Discretized at fixed dt this collapses to a one-pole blend:
const float TAU = 1.0f; // s — crossover time constant
const float DT = 0.01f; // 100 Hz fixed loop
const float ALPHA = TAU / (TAU + DT); // = 0.9901
pitch = ALPHA * (pitch + gyDps * DT) + (1.0f - ALPHA) * pitchAcc;
roll = ALPHA * (roll + gxDps * DT) + (1.0f - ALPHA) * rollAcc;
Below ~tau seconds the estimate follows the gyro (smooth), beyond ~tau it settles to the accelerometer (absolute). tau = 1.0 s is deliberately slow for a device read at rest: strong rejection of hand tremor and surface vibration, while gyro bias still washes out within a second — faster than you can read the display. The same structure, with faster tau and a state-space dressing, is exactly what the TWIP/ballbot estimators run before their LQR/PPO loops.
Calibration is a two-part tare captured in one button press while the device
sits still: 100 samples average the raw gyro rates into per-axis bias (fed
back into the filter’s rate input) and the accel angles into pitch/roll
offsets (subtracted only at display time, so integration stays in the sensor
frame). Both persist in ESP32 NVS via Preferences:
gyroBiasY = sumGy / N; // deg/s — removed before integration
pitchOffset = sumPitch / N; // deg — tare, subtracted for display/beep
prefs.putFloat("pOff", pitchOffset);
The level beep uses hysteresis — trigger inside ±0.2°, release outside ±0.35° — so the tone doesn’t chatter on the boundary.
SIMULATION
The build runs unmodified in Wokwi — all five parts exist as native Wokwi
components (board-esp32-devkit-c-v4, wokwi-mpu6050, wokwi-ssd1306,
wokwi-pushbutton, wokwi-buzzer), no substitutions. Steps: new ESP32
project on wokwi.com, paste sketch.ino and diagram.json from
sources/electronics/digital-level/, press Play. Click the MPU6050 part and
drag its simulated accelerometer axes in the part inspector to tilt the
virtual board — the bubble slides off center and the numeric pitch/roll
follows. Return it to flat and the buzzer holds its 2.2 kHz level tone. The
blue ZERO CAL button tares any pose to zero (two confirmation beeps).
STATUS
Design and simulation only. The firmware is complete and exercised in Wokwi (tilt response, tare, level beep); no physical unit has been assembled and no bench accuracy characterization exists. The ±0.2° level window and 0.1° display resolution are design intent, not verified accuracy — the MPU6050’s datasheet-typical offset drift and cross-axis terms put the realistic post-tare accuracy in the ±0.5° class, and mounting flatness of a real enclosure would dominate the error budget before the sensor does.
USE CASES & APPLICATIONS
Machine and equipment setup (leveling a lathe bed, printer gantry, or wash machine feet with a live numeric readout), camera gimbal and tripod horizon alignment, construction layout where a beeping level frees the eyes, and RV or trailer leveling. The filter itself is the wider payoff: the same accel-plus-gyro complementary structure is the entry-level attitude estimator in drones, self-balancing robots, and every hobby IMU stack — this project is that estimator isolated, instrumented, and made legible.
FILES
sources/electronics/digital-level/sketch.ino— firmware: raw MPU6050 I2C driver, complementary filter with derivation in the header, NVS-backed calibration, OLED bubble UI, beep hysteresis.sources/electronics/digital-level/diagram.json— Wokwi wiring diagram, pin-identical to the sketch.sources/electronics/digital-level/README.md— Wokwi run steps, library list, real-build and accuracy notes (datasheet-typical, labeled as such).