Files
air-mouse/source/sleep.h

109 lines
4.9 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* sleep.h — IMU Mouse low-power manager
* =====================================================================
* Two-stage sleep strategy for the Seeed XIAO nRF52840 Sense:
*
* STAGE 1 — IMU low-power (entered after SLEEP_IMU_IDLE_MS of no motion)
* • Gyroscope powered down (CTRL2_G ODR = 0000)
* • Accelerometer → LP 26 Hz (CTRL1_XL ODR = 0010, XL_HM_MODE=1)
* • LSM6DS3 wakeup-interrupt armed (MD1_CFG INT1_WU=1)
* • nRF52840 stays awake, BLE advertising/connected continues
* • Current: ~0.19 mA accel only vs ~0.90 mA combo HP
*
* STAGE 2 — System deep sleep (entered after SLEEP_DEEP_IDLE_MS of no motion)
* • Only entered when BLE is NOT connected (i.e. no web-UI/host attached)
* • Gyro still off, accel still in LP
* • nRF52840 goes into sd_app_evt_wait() — SoftDevice manages radio
* • Wake: IMU INT1 GPIO interrupt → ISR sets wakeFlag, loop resumes
* • On wake: gyro re-enabled, full-rate accel restored, bias re-calibrated
*
* Integration
* -----------
* 1. #include "sleep.h" in main.cpp (already done below)
* 2. Call sleepManagerInit() once in setup(), after imu.begin().
* 3. Call sleepManagerUpdate(now, idle, Bluefruit.connected())
* at the top of loop() (before the early-return on LOOP_RATE_MS).
* 4. The manager returns immediately; it never blocks the loop.
*
* Pin assignment
* --------------
* LSM6DS3 INT1 → XIAO P0.11 (digital pin 3 on the 10-pin header)
* Change IMU_INT1_PIN below if your wiring differs.
* =====================================================================
*/
#pragma once
#include <Arduino.h>
// ── Tuning ──────────────────────────────────────────────────────────────────
// Time of no-motion before each sleep stage kicks in.
// These are deliberately conservative — tighten to taste.
#ifndef SLEEP_IMU_IDLE_MS
#define SLEEP_IMU_IDLE_MS (10UL * 1000UL) // 10 s → gyro off, accel LP
#endif
#ifndef SLEEP_DEEP_IDLE_MS
#define SLEEP_DEEP_IDLE_MS (60UL * 1000UL) // 60 s → system deep sleep (no-BLE only)
#endif
// LSM6DS3 wakeup threshold: 1 LSB = 7.8 mg at ±2 g FS (±2g range).
// The wakeup engine uses a slope filter (difference between consecutive samples
// at 26 Hz, so each sample is ~38 ms apart).
// Too low = wakes on keyboard/desk vibration. Too high = misses gentle pick-up.
// 8 LSB × 7.8 mg ≈ 62 mg — filters desk vibration, fires on deliberate movement.
// Raise to 1216 if still waking from vibration; lower to 4 if too sluggish.
#ifndef SLEEP_WAKEUP_THS
#define SLEEP_WAKEUP_THS 16 // 063
#endif
// Number of consecutive 26 Hz samples that must exceed the threshold.
// 2 samples = ~77 ms of sustained movement required before wakeup fires.
// This is the most effective filter against single-shot vibration spikes
// (keyboard strikes, desk bumps) which are impulsive rather than sustained.
#ifndef SLEEP_WAKEUP_DUR
#define SLEEP_WAKEUP_DUR 2 // 03
#endif
// GPIO pin connected to LSM6DS3 INT1.
// On XIAO nRF52840 Sense, INT1 = P0.11 (internal trace, not on user header).
// The Adafruit nRF52 BSP exposes this as PIN_LSM6DS3TR_C_INT1 — always use
// that constant, never a bare number whose Arduino index is BSP-dependent.
#ifndef IMU_INT1_PIN
#define IMU_INT1_PIN PIN_LSM6DS3TR_C_INT1
#endif
// ── Public state (read-only from main.cpp) ───────────────────────────────────
enum SleepStage : uint8_t {
SLEEP_AWAKE = 0, // normal full-rate operation
SLEEP_IMU_LP = 1, // gyro off, accel LP — nRF awake
SLEEP_DEEP = 2, // system WFE — BLE disconnected only
};
extern volatile SleepStage sleepStage;
extern volatile bool imuWakeFlag; // set by INT1 ISR, cleared by manager
// ── API ──────────────────────────────────────────────────────────────────────
/**
* Call once in setup() after imu.begin().
* Configures INT1 GPIO and arms the LSM6DS3 wakeup interrupt (always-on,
* even in normal mode — it simply won't fire unless the device is still).
*/
void sleepManagerInit();
/**
* Call every loop() iteration.
* @param nowMs millis() timestamp
* @param idle true when the motion pipeline reports no cursor movement
* @param bleConnected Bluefruit.connected()
*
* Returns true if the caller should skip IMU reads this iteration
* (i.e. we are in SLEEP_IMU_LP or just woke up and are re-initialising).
*/
bool sleepManagerUpdate(unsigned long nowMs, bool idle, bool bleConnected);
/**
* Re-arms the LSM6DS3 after wake-from-deep-sleep.
* Called internally by sleepManagerUpdate(); exposed so calibrateGyroBias()
* can also call it if it needs to know sleep state.
*/
void sleepManagerWakeIMU();