diff --git a/docs/IMU-QMI8658-QMC6310.md b/docs/IMU-QMI8658-QMC6310.md new file mode 100644 index 000000000..16f127557 --- /dev/null +++ b/docs/IMU-QMI8658-QMC6310.md @@ -0,0 +1,211 @@ +# IMU and Magnetometer Integration (QMI8658 + QMC6310) + +This document explains the implementation work added for the LilyGo T-Beam S3 Supreme to support: + +- QMI8658 6‑axis IMU over SPI (accelerometer + gyroscope) with a debug stream and a UI page +- QMC6310 3‑axis magnetometer over I2C with live hard‑iron calibration, heading computation, and a UI page +- I2C scanner improvements for robust IMU detection +- A small “live data” layer so UI screens do not reset sensors + +The focus is on the math used and how the code is wired together. + +--- + +## Files and Components + +- QMI8658 (SPI) driver wrapper + - `src/motion/QMI8658Sensor.h/.cpp` +- QMC6310 (I2C) driver wrapper + - `src/motion/QMC6310Sensor.h/.cpp` +- Live data shared with UI (prevents sensor resets by UI) + - `src/motion/SensorLiveData.h/.cpp` (globals `g_qmi8658Live`, `g_qmc6310Live`) +- I2C scanner and main wiring + - `src/detect/ScanI2CTwoWire.cpp`, `src/detect/ScanI2C.cpp`, `src/main.cpp` +- UI Screens (added after the GPS screen) + - `src/graphics/draw/DebugRenderer.h/.cpp` + - `src/graphics/Screen.cpp` + +Dependency pulled via PlatformIO: + +- Lewis He SensorLib (provides `SensorQMI8658.hpp`, `SensorQMC6310.hpp`) pinned in `platformio.ini`. + +--- + +## QMI8658 (SPI) – Implementation and Math + +### Bus + Initialization + +- On ESP32‑S3, we use HSPI for the IMU to avoid clashing with radio SPI: + - Pins (T‑Beam S3 Supreme): `MOSI=35`, `MISO=37`, `SCK=36`, `IMU_CS=34`. + - Code creates a local `SPIClass(HSPI)` and calls `begin(SCK, MISO, MOSI, -1)`; sets `IMU_CS` HIGH. +- The driver is configured as: + - Accelerometer range: ±4 g, ODR ≈ 1000 Hz, LPF mode 0 + - Gyroscope range: ±64 dps, ODR ≈ 897 Hz, LPF mode 3 +- The thread (`QMI8658Sensor`) runs continuously. When `QMI8658_DEBUG_STREAM` is enabled, it samples every pass but logs once per second. + +### Units + +- Accelerometer is reported in m/s² by the library. To measure total acceleration (for wake‑on‑motion), we compute: + + ``` + |a| = sqrt(ax² + ay² + az²) + |a|_g = |a| / 9.80665 + Δ = |a|_g − 1.0 + ``` + + If `Δ` exceeds a small threshold (`0.15 g`), we wake the screen. + +### Debug Stream + +- The debug line (1 Hz) prints: + + `QMI8658: ready=<0/1> ACC[x y z] m/s^2 GYR[x y z] dps` + + This is also mirrored into the live data struct `g_qmi8658Live` that the UI reads. + +--- + +## QMC6310 (I2C) – Implementation and Math + +### Bus + Initialization + +- Address: `0x1C` on the sensors bus (Wire). The I2C scanner detects it and exposes it as `ScanI2C::QMC6310`. +- Configuration via SensorLib wrapper: + - Mode: continuous + - Range: 2 G + - ODR: 50 Hz + - Oversample: 8×, Downsample: 1× + +### Hard‑Iron Calibration (live) + +We continuously track min/max per axis and compute offsets as the center: + +``` +minX = min(minX, rawX) maxX = max(maxX, rawX) +minY = min(minY, rawY) maxY = max(maxY, rawY) +minZ = min(minZ, rawZ) maxZ = max(maxZ, rawZ) + +offsetX = (maxX + minX) / 2 +offsetY = (maxY + minY) / 2 +offsetZ = (maxZ + minZ) / 2 + +mx = rawX − offsetX +my = rawY − offsetY +mz = rawZ − offsetZ +``` + +This removes hard‑iron bias (DC offset) and is adequate for real‑time heading stabilization. For best results, slowly rotate the device on all axes for several seconds to let min/max settle. + +Soft‑iron distortion (elliptical scaling) is NOT corrected here. A future enhancement can compute per‑axis scale from `(max−min)/2` or use an ellipsoid fit. + +### Heading Computation + +Raw 2‑D horizontal heading (no tilt compensation): + +``` +heading_deg = atan2(my, mx) * 180/π +heading_true = wrap_0_360( heading_deg + declination_deg + yaw_mount_offset ) +``` + +Where: + +- `declination_deg` compensates for local magnetic declination (positive East, negative West). We support a build‑time macro `QMC6310_DECLINATION_DEG`. +- `yaw_mount_offset` lets you nudge heading for how the board is mounted; build‑time macro `QMC6310_YAW_MOUNT_OFFSET`. +- `wrap_0_360(θ)` folds θ into `[0, 360)` by repeated add/subtract 360. + +Screen orientation (0/90/180/270) is applied after heading is computed and normalized. + +### Tilt‑Compensated Heading (future option) + +If pitch/roll are available (from IMU), heading can be tilt‑compensated: + +``` +mx' = mx*cos(θ) + mz*sin(θ) +my' = mx*sin(φ)*sin(θ) + my*cos(φ) − mz*sin(φ)*cos(θ) +heading = atan2(my', mx') +``` + +Where `φ` is roll and `θ` is pitch (radians), derived from accelerometer. This is not implemented yet but can be added. + +### Live Data + +The magnetometer thread writes the latest raw XYZ, offsets and heading into `g_qmc6310Live` for the UI to display without touching hardware. + +--- + +## I2C Scanner Improvements + +### Dual‑address detection for IMUs + +- QMI8658 is now probed at both `0x6B` and `0x6A`. +- To avoid collisions with chargers on `0x6B`, we first check: + - BQ24295 ID via register `0x0A == 0xC0` + - BQ25896 via `0x14` (bits 1:0 == `0b10`) +- If not a charger, we read `0x0F`: + - `0x6A` → classify as LSM6DS3 + - otherwise → classify as QMI8658 + +### IMU‑only late rescan + +- After a normal pass, if neither QMI8658 nor LSM6DS3 is found on a port, we wait 700 ms and probe just `0x6A/0x6B` again. This helps boards that power the IMU slightly late. + +--- + +## UI Screens + +Two additional screens are inserted right after the GPS screen: + +1) QMC6310 screen + - Shows `Heading`, `offX/offY`, and `rawX/rawY` (1 Hz) + - Data source: `g_qmc6310Live` + +2) QMI8658 screen + - Shows `ACC x/y/z` (m/s²) and `GYR x/y/z` (dps) (1 Hz) + - Data source: `g_qmi8658Live` + +Because these screens read the live data structs, they do NOT call `begin()` on sensors (which would reset them). This resolved the issue where the screen showed all zeros after switching. + +--- + +## Build Flags and Configuration + +- Global debug stream (QMI8658): + - `-D QMI8658_DEBUG_STREAM` (enabled in the T‑Beam S3 Core variant) + - When enabled, the main will also start a parallel IMU debug thread even if an I2C accelerometer/magnetometer is present. + +- Declination and mount offset for QMC6310 heading (optional): + - `-D QMC6310_DECLINATION_DEG=` (e.g., `-0.25` for ≈ 0°15′ W) + - `-D QMC6310_YAW_MOUNT_OFFSET=` (tune to match a known reference) + +- SensorLib dependency (Lewis He): + - Pinned in `platformio.ini` under `[arduino_base]`: + `https://github.com/lewisxhe/SensorLib/archive/769b48472278aeaa62d5d0526eccceb74abe649a.zip` + +--- + +## Example Logs + +``` +QMC6310: head=137.5 off[x=-12900 y=9352 z=12106] raw[x=-12990 y=9435 z=12134] +QMI8658: ready=1 ACC[x=-0.782 y=0.048 z=0.539] m/s^2 GYR[x=11.742 y=4.570 z=1.836] dps +``` + +The values on the UI screens should match these, because both screens read from the live data updated by the threads. + +--- + +## Known Limitations and Next Steps + +- QMC6310 uses live hard‑iron calibration only; no soft‑iron compensation yet. We can add per‑axis scale from `(max−min)/2` or use an ellipsoid fit for better accuracy. +- Heading is not tilt‑compensated; adding pitch/roll from the IMU and applying the standard compensation will stabilize heading during motion. +- We currently do not persist calibration offsets across boots; adding storage (NVS) would improve user experience. +- The QMI8658 debug stream is designed for development and can be disabled via build flag to reduce log noise. + +--- + +## Troubleshooting + +- If QMI8658 shows zeros on the UI screen, ensure `QMI8658_DEBUG_STREAM` is enabled or let the background IMU thread initialize first (it sets `g_qmi8658Live.initialized`). +- If QMC6310 heading appears constrained or jumps, rotate the device slowly on all axes for 10–20 seconds to update min/max; verify you’re on the correct I2C port and address (`0x1C`). +- If the I2C scan does not find the IMU on power‑on, check that the late‑rescan log appears; some boards power the sensor rail slightly later. +