A precision circular bubble level driven by the QMI8658 accelerometer — color-coded tilt zones, low-pass smoothing, and tap-to-calibrate at ~60 fps.
The QMI8658 is configured in accelerometer-only mode (2 g range, 1000 Hz ODR) for maximum leveling precision. Pitch and roll are derived from the three gravity components using atan2, then fed through a high-coefficient low-pass filter (α = 0.85) to produce a smooth, jitter-free bubble.
The bubble is drawn inside a circular bowl with degree rings at ±5° and ±10°. Its color shifts from green (level) through yellow and orange to red as tilt increases. Tapping the screen zeroes the current tilt as the new reference — useful when mounting on an already-sloped surface.
| Total tilt | Color | Meaning |
|---|---|---|
| < 1.0° | Green | Level — display also shows * LEVEL * banner |
| 1.0° – 3.0° | Yellow | Slightly off — usable for most applications |
| 3.0° – 7.0° | Orange | Noticeable tilt — needs adjustment |
| > 7.0° | Red | Significantly tilted |
| Stage | Detail |
|---|---|
| IMU read | 6 bytes from QMI8658 at 0x35. Accel-only mode; 2 g range → 16384 LSB/g. Gyro disabled (CTRL7 = 0x01) to reduce noise floor. |
| Angle calc | pitch = atan2(ay, √(ax²+az²)) × 180/π | roll = atan2(−ax, az) × 180/π |
| Low-pass filter | smooth = 0.85 × prev + 0.15 × raw. High alpha trades lag for stability — ideal for a level where fast response is less important than steadiness. |
| Calibration offset | On tap: calPitch = smoothPitch; calRoll = smoothRoll. Displayed angle = smooth − cal. |
| Bubble clamp | Bubble pixel offset capped at BOWL_R − BUBBLE_R − 4 px so it never exits the bowl regardless of actual tilt angle. |
CTRL1 (0x02) = 0x40 // auto-increment CTRL2 (0x03) = 0x03 // 2g, 1000 Hz ODR CTRL7 (0x08) = 0x01 // accel only
Gyro is left off — it contributes noise without benefit when only static tilt is needed.
±2 g range → 16384 LSB/g (vs 8192 LSB/g for ±4 g used in other sketches) 2× finer resolution at low tilt → bubble reacts to <0.06° steps
CX = 120, CY = 128 // bowl center BOWL_R = 95 px // outer radius BUBBLE_R = 14 px // bubble radius RANGE = 15.0° // deg at bowl edge
// Ring radius for N degrees: r = N / RANGE × (BOWL_R − BUBBLE_R − 4) 5° ring → ~27 px from center 10° ring → ~53 px from center
All drawing targets an Arduino_Canvas off-screen buffer in PSRAM. Each frame: fill background, draw bowl rings and crosshair, place bubble with shadow and highlight glint, render pitch/roll readouts, then canvas->flush() in one DMA burst. At 16 ms per loop this achieves ~60 fps with no visible tearing.
Full sketch in the ESP32-S3-LCD repo — sketches/spirit_level/spirit_level.ino.
git clone https://github.com/binRick/ESP32-S3-LCD bash flash-sketch-004-spirit-level.sh