109 lines
4.9 KiB
C
109 lines
4.9 KiB
C
/*
|
||
* 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 12–16 if still waking from vibration; lower to 4 if too sluggish.
|
||
#ifndef SLEEP_WAKEUP_THS
|
||
#define SLEEP_WAKEUP_THS 16 // 0–63
|
||
#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 // 0–3
|
||
#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(); |