Sleep Current of XIAO nRF52840, Deep Sleep vs. Light Sleep

Hi there,

SO forget the coin cell, I mean you can get it too work I have and others too, but the juice isn’t there for the squeeze (long haul) . Lipo is made for it and they come in every size and capacity you would need, can even get one shaped like a dog bone. (don’t ask) :grin:

let me test it out.

HTH
GL :slight_smile: PJ :v:

are you using a PPKII (power profiler) ? and which BSP ?
lmk

1 Like

Hi there,

SO it compiles , with BSP

FQBN: Seeeduino:mbed:xiaonRF52840Sense Using board 'xiaonRF52840Sense' from platform in folder: 
C:\Users\Dude\AppData\Local\Arduino15\packages\Seeeduino\hardware\mbed\2.9.3
 Using core 'arduino' from platform in folder: 
C:\Users\Dude\AppData\Local\Arduino15\packages\Seeeduino\hardware\mbed\2.9.3

I had to roll back the LIB for the IMU to get it to build without errors

C:\\Users\\Dude\\AppData\\Local\\Arduino15\\packages\\Seeeduino\\tools\\arm-none-eabi-gcc\\7-2017q4/bin/arm-none-eabi-size" -A 
"C:\\Users\\Dude\\AppData\\Local\\arduino\\sketches\\92F550704A3A6407E31355579F4A3705/sketch_mar30a.ino.elf" 
Sketch uses 338752 bytes (41%) of program storage space.
Maximum is 811008 bytes. 
Global variables use 71896 bytes (30%) of dynamic memory, leaving 165672 bytes for local variables. Maximum is 237568 bytes. Performing 1200-bps touch reset on serial port COM12 Waiting for upload port... 
Upload port found on COM15 
"C:\Users\Dude\AppData\Local\Arduino15\packages\Seeeduino\hardware\mbed\2.9.3/tools/adafruit-nrfutil/win32/adafruit-nrfutil.exe" --verbose dfu serial -pkg "C:\Users\Dude\AppData\Local\arduino\sketches\92F550704A3A6407E31355579F4A3705/sketch_mar30a.ino.zip" -p COM15 -b 115200 --singlebank Upgrading target on COM15 with DFU package C:\Users\Dude\AppData\Local\arduino\sketches\92F550704A3A6407E31355579F4A3705\sketch_mar30a.ino.zip. 
Flow control is disabled, Single bank, Touch disabled 
Opened serial port COM15 Starting DFU upgrade of type 4, SoftDevice size: 0, bootloader size: 0, application size: 338760 
Sending DFU start packet 
Sending DFU init packet Sending firmware file ######################################## " 

Now let’s dig into the power and sleep business. I have a PPKII hooked up to it now. so I can run it and observer the battery power and sleep currents

Confirmed your 8.4ma. RUN. BLE connected


Now for the code. tests.
:v:

HTH
GL :slight_smile: PJ :+1:

Hi there,

So, I tested and retested and I found some stuff.

XIAO nRF52840 Sense – Sleep Current Investigation

T1) Baseline: ~736–746 μA (SYSTEMOFF confirmed)
T2) BLE Test: Active 4–8 mA, sleep returns to ~746 μA
T3) GPIO Wake: Returns to ~746 μA
T4) Full App (no IMU): ~746 μA
T5) IMU Initialized (no shutdown): ~1.13 mA (+~400 μA increase)
T6) IMU Power Only: ~746 μA (no impact)
T7) Forced I2C Shutdown: ~10 mA (invalid state, not usable) :backhand_index_pointing_left: :face_with_peeking_eye:
T8) Correct Sequence (IMU standby + power off): ~736 μA (resolved)
Conclusion: IMU must be placed in standby before power removal. I2C lines can back-power the sensor if not properly shut down. Avoid Wire.end() on this BSP. :flexed_biceps:

Test 1

something is up on this IMU?


Fixed , getting closer…

/*
 * ------------------------------------------------------------------
 * Project : WinkelSensor BLE Tilt Beacon
 * Target  : Seeed XIAO nRF52840 Sense
 * BSP     : Seeeduino mbed 2.9.x
 * IMU     : LSM6DS3 over I2C (0x6A)  2.0.4 
 *
 * Function:
 * - Reads tilt angle from onboard IMU
 * - Advertises angle and battery voltage over BLE
 * - Enters SYSTEMOFF on long button press or low battery
 * - Supports charge-wait mode when USB is present and battery is low
 *
 * Low Power Notes:
 * - Baseline SYSTEMOFF measured: ~736–746 uA
 * - If IMU is initialized but not explicitly shut down,
 *   sleep current can remain around ~1.13 mA
 * - IMU must be placed into standby before power removal
 * - Do NOT call Wire.end() for shutdown on this BSP
 *
 * Verified with Nordic Power Profiler Kit II (PPK2)
 * ------------------------------------------------------------------
 */

#include <Arduino.h>
#include <ArduinoBLE.h>
#include <Wire.h>
#include "LSM6DS3.h"
#include <math.h>
#include <nrf_gpio.h>

static const uint8_t SENSOR_ID = 1;

static const uint32_t SEND_MS              = 500;
static const uint32_t BATT_MEASURE_MS      = 5000;
static const uint32_t CHARGE_WAIT_CHECK_MS = 10000;
static const uint32_t SLEEP_HOLD_MS        = 3000;

static const uint16_t LOW_BATT_SLEEP_MV = 3650;
static const uint16_t LOW_BATT_LED_MV   = 3730;
static const uint16_t RESUME_BATT_MV    = 3800;

// Complementary filter
static const float ALPHA        = 0.985f;
static const float SMOOTH_ALPHA = 0.18f;

// 100k / 100k divider => factor 2.0
static const int   BAT_PIN            = A0;
static const float ADC_REF_V          = 3.3f;
static const float BAT_DIVIDER_FACTOR = 2.0f;
static const float BAT_CAL_FACTOR     = 1.00f;
static const int   ADC_MAX            = 4095;

// External LEDs
static const int LED_BLUE_PIN  = A1;
static const int LED_RED_PIN   = A2;
static const int LED_GREEN_PIN = A3;

// Button: D5 -> button -> GND
static const int BUTTON_PIN = D5;

static const uint8_t FLAG_CHARGING = 0x01;

// LSM6DS3 registers used for standby
static const uint8_t IMU_ADDR      = 0x6A;
static const uint8_t REG_CTRL1_XL  = 0x10;
static const uint8_t REG_CTRL2_G   = 0x11;

LSM6DS3 imu(I2C_MODE, IMU_ADDR);

struct __attribute__((packed)) AdvPacket {
  uint8_t  magic1;
  uint8_t  magic2;
  uint8_t  version;
  uint8_t  id;
  int16_t  angle_ddeg;
  uint16_t batt_mv;
  uint16_t seq;
  uint8_t  flags;
};

enum SensorMode {
  MODE_NORMAL = 0,
  MODE_CHARGE_WAIT = 1
};

static SensorMode currentMode = MODE_NORMAL;

static uint16_t seqNo              = 0;
static uint32_t nextSendMs         = 0;
static uint32_t nextBattMs         = 0;
static uint32_t nextChargeCheckMs  = 0;
static uint32_t lastFilterUs       = 0;

static float pitchDeg          = 0.0f;
static float smoothPitchDeg    = 0.0f;
static bool  filterInitialized = false;
static bool  imuReady          = false;
static float gyroOffsetY       = 0.0f;
static uint16_t batteryMv      = 0;

volatile bool buttonPressed = false;

void buttonISR() {
  buttonPressed = true;
}

static void blinkInternalLed(uint8_t ledPin, int times, int delayMs) {
  pinMode(ledPin, OUTPUT);
  for (int i = 0; i < times; i++) {
    digitalWrite(ledPin, LOW);   // internal RGB is active-low
    delay(delayMs);
    digitalWrite(ledPin, HIGH);
    delay(delayMs);
  }
}

static void internalRgbOff() {
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(LED_BLUE, OUTPUT);

  digitalWrite(LED_RED, HIGH);
  digitalWrite(LED_GREEN, HIGH);
  digitalWrite(LED_BLUE, HIGH);
}

static void parkInternalRgb() {
  internalRgbOff();

  nrf_gpio_cfg_sense_input(digitalPinToPinName(LED_RED),   NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_NOSENSE);
  nrf_gpio_cfg_sense_input(digitalPinToPinName(LED_GREEN), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_NOSENSE);
  nrf_gpio_cfg_sense_input(digitalPinToPinName(LED_BLUE),  NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_NOSENSE);
}

static void ledsOff() {
  digitalWrite(LED_BLUE_PIN, LOW);
  digitalWrite(LED_RED_PIN, LOW);
  digitalWrite(LED_GREEN_PIN, LOW);
}

static void setLedNormal() {
  digitalWrite(LED_BLUE_PIN, LOW);
  digitalWrite(LED_RED_PIN, LOW);
  digitalWrite(LED_GREEN_PIN, HIGH);
}

static void setLedCharging() {
  digitalWrite(LED_BLUE_PIN, HIGH);
  digitalWrite(LED_RED_PIN, LOW);
  digitalWrite(LED_GREEN_PIN, LOW);
}

static void setLedLowBattery() {
  digitalWrite(LED_BLUE_PIN, LOW);
  digitalWrite(LED_RED_PIN, HIGH);
  digitalWrite(LED_GREEN_PIN, LOW);
}

static void parkBoardPinsForSleep() {
  pinMode(LED_BLUE_PIN, INPUT);
  pinMode(LED_RED_PIN, INPUT);
  pinMode(LED_GREEN_PIN, INPUT);
  pinMode(A0, INPUT);

#ifdef PIN_VBAT_ENABLE
  pinMode(PIN_VBAT_ENABLE, OUTPUT);
  digitalWrite(PIN_VBAT_ENABLE, HIGH);
#endif

#ifdef VBAT_ENABLE
  pinMode(VBAT_ENABLE, OUTPUT);
  digitalWrite(VBAT_ENABLE, HIGH);
#endif

#ifdef PIN_CHARGING_CURRENT
  pinMode(PIN_CHARGING_CURRENT, INPUT);
#endif

#ifdef PIN_LSM6DS3TR_C_POWER
  pinMode(PIN_LSM6DS3TR_C_POWER, OUTPUT);
  digitalWrite(PIN_LSM6DS3TR_C_POWER, LOW);
#endif

#ifdef PIN_PDM_PWR
  pinMode(PIN_PDM_PWR, OUTPUT);
  digitalWrite(PIN_PDM_PWR, LOW);
#endif

#ifdef PIN_QSPI_CS
  pinMode(PIN_QSPI_CS, INPUT_PULLUP);
#endif
#ifdef PIN_QSPI_SCK
  pinMode(PIN_QSPI_SCK, INPUT);
#endif
#ifdef PIN_QSPI_IO0
  pinMode(PIN_QSPI_IO0, INPUT);
#endif
#ifdef PIN_QSPI_IO1
  pinMode(PIN_QSPI_IO1, INPUT);
#endif
#ifdef PIN_QSPI_IO2
  pinMode(PIN_QSPI_IO2, INPUT);
#endif
#ifdef PIN_QSPI_IO3
  pinMode(PIN_QSPI_IO3, INPUT);
#endif
}

static float clampPitch(float deg) {
  if (deg > 90.0f) return 90.0f;
  if (deg < -90.0f) return -90.0f;
  return deg;
}

static int16_t degToDdeg(float deg) {
  float v = deg * 10.0f;
  if (v > 900.0f)  v = 900.0f;
  if (v < -900.0f) v = -900.0f;
  return (int16_t)lroundf(v);
}

static bool usbPresent() {
  return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
}

static uint8_t makeFlags(bool charging) {
  uint8_t f = 0;
  if (charging) f |= FLAG_CHARGING;
  return f;
}

static uint16_t measureBatteryMv() {
  const int samples = 32;
  uint32_t sum = 0;

  analogRead(BAT_PIN);
  delay(2);

  for (int i = 0; i < samples; i++) {
    sum += analogRead(BAT_PIN);
    delay(2);
  }

  float raw  = sum / float(samples);
  float vAdc = (raw * ADC_REF_V) / ADC_MAX;
  float vBat = vAdc * BAT_DIVIDER_FACTOR * BAT_CAL_FACTOR;

  if (vBat < 0.0f) vBat = 0.0f;
  return (uint16_t)lroundf(vBat * 1000.0f);
}

static float readAccelPitchDeg() {
  float ax = imu.readFloatAccelX();
  float ay = imu.readFloatAccelY();
  float az = imu.readFloatAccelZ();

  float accPitch = atan2f(ax, sqrtf(ay * ay + az * az)) * 180.0f / PI;
  return clampPitch(accPitch);
}

static void calibrateGyro() {
  const int samples = 500;
  float sum = 0.0f;

  for (int i = 0; i < samples; i++) {
    sum += imu.readFloatGyroY();
    delay(2);
  }

  gyroOffsetY = sum / samples;
}

static bool initImuIfNeeded() {
  if (imuReady) return true;

#ifdef PIN_LSM6DS3TR_C_POWER
  pinMode(PIN_LSM6DS3TR_C_POWER, OUTPUT);
  digitalWrite(PIN_LSM6DS3TR_C_POWER, HIGH);
  delay(10);
#endif

  if (imu.begin() != 0) {
    Serial.println("IMU init failed");
    return false;
  }

  Serial.println("Keep sensor still for gyro calibration...");
  delay(1000);
  calibrateGyro();

  pitchDeg = readAccelPitchDeg();
  smoothPitchDeg = pitchDeg;
  lastFilterUs = micros();
  filterInitialized = true;
  imuReady = true;
  return true;
}

static void updatePitchFilter() {
  uint32_t nowUs = micros();

  if (!filterInitialized) {
    pitchDeg = readAccelPitchDeg();
    smoothPitchDeg = pitchDeg;
    lastFilterUs = nowUs;
    filterInitialized = true;
    return;
  }

  float dt = (nowUs - lastFilterUs) / 1000000.0f;
  lastFilterUs = nowUs;

  if (dt <= 0.0f || dt > 0.1f) dt = 0.01f;

  float gyroY = imu.readFloatGyroY() - gyroOffsetY;
  float accelPitch = readAccelPitchDeg();

  float gyroPitch = pitchDeg + gyroY * dt;
  pitchDeg = ALPHA * gyroPitch + (1.0f - ALPHA) * accelPitch;
  pitchDeg = clampPitch(pitchDeg);

  smoothPitchDeg += SMOOTH_ALPHA * (pitchDeg - smoothPitchDeg);
  smoothPitchDeg = clampPitch(smoothPitchDeg);
}

static void imuStandbyForSleep() {
  if (!imuReady) {
    return;
  }

  // Accel OFF
  Wire.beginTransmission(IMU_ADDR);
  Wire.write(REG_CTRL1_XL);
  Wire.write(0x00);
  Wire.endTransmission();
  delay(5);

  // Gyro OFF
  Wire.beginTransmission(IMU_ADDR);
  Wire.write(REG_CTRL2_G);
  Wire.write(0x00);
  Wire.endTransmission();
  delay(10);

  filterInitialized = false;
  imuReady = false;
}

static void advertisePacket(int16_t angleDdeg, uint8_t flags) {
  AdvPacket p;
  p.magic1 = 'W';
  p.magic2 = 'H';
  p.version = 3;
  p.id = SENSOR_ID;
  p.angle_ddeg = angleDdeg;
  p.batt_mv = batteryMv;
  p.seq = seqNo++;
  p.flags = flags;

  BLE.stopAdvertise();
  BLE.setManufacturerData((const uint8_t*)&p, sizeof(p));
  BLE.advertise();
}

static void enterTrueSystemOff() {
  BLE.stopAdvertise();
  delay(20);

  detachInterrupt(digitalPinToInterrupt(BUTTON_PIN));
  buttonPressed = false;

  // Critical low-power fix:
  // put IMU into standby BEFORE cutting the sensor rail
  imuStandbyForSleep();
  delay(20);

  ledsOff();
  internalRgbOff();
  parkInternalRgb();
  parkBoardPinsForSleep();

  NRF_POWER->DCDCEN  = 1;
  NRF_POWER->DCDCEN0 = 1;

  delay(20);

  nrf_gpio_cfg_sense_input(
    digitalPinToPinName(BUTTON_PIN),
    NRF_GPIO_PIN_PULLUP,
    NRF_GPIO_PIN_SENSE_LOW
  );

  NRF_POWER->SYSTEMOFF = 1;

  while (1) {
    __WFE();
  }
}

static void enterLowBatterySystemOff() {
  setLedLowBattery();
  delay(300);
  enterTrueSystemOff();
}

static bool handleSleepButton() {
  if (!buttonPressed) return false;
  buttonPressed = false;

  unsigned long pressStart = millis();
  while (digitalRead(BUTTON_PIN) == LOW && millis() - pressStart < SLEEP_HOLD_MS) {
    delay(10);
  }

  if (digitalRead(BUTTON_PIN) == LOW && millis() - pressStart >= SLEEP_HOLD_MS) {
    blinkInternalLed(LED_RED, 2, 120);

    while (digitalRead(BUTTON_PIN) == LOW) {
      delay(1);
    }
    delay(50);

    enterTrueSystemOff();
    return true;
  }

  return false;
}

static void switchToChargeWait() {
  currentMode = MODE_CHARGE_WAIT;
  nextChargeCheckMs = millis();
  setLedCharging();
}

static void switchToNormal() {
  if (!initImuIfNeeded()) return;

  currentMode = MODE_NORMAL;
  uint32_t now = millis();
  nextSendMs = now + SEND_MS;
  nextBattMs = now + BATT_MEASURE_MS;

  setLedNormal();
  advertisePacket(degToDdeg(smoothPitchDeg), makeFlags(usbPresent()));
}

static void evaluatePowerState(bool allowAdvertiseStatus) {
  bool usb = usbPresent();

  if (!usb && batteryMv <= LOW_BATT_SLEEP_MV) {
    enterLowBatterySystemOff();
    return;
  }

  if (usb && batteryMv < RESUME_BATT_MV) {
    switchToChargeWait();
    if (allowAdvertiseStatus) {
      advertisePacket(0, makeFlags(true));
    }
    return;
  }

  switchToNormal();
}

void setup() {
  Serial.begin(115200);
  delay(300);

  pinMode(BUTTON_PIN, INPUT_PULLUP);

  while (digitalRead(BUTTON_PIN) == LOW) {
    delay(1);
  }
  delay(50);

  internalRgbOff();

  if (NRF_POWER->RESETREAS & POWER_RESETREAS_OFF_Msk) {
    NRF_POWER->RESETREAS = POWER_RESETREAS_OFF_Msk;
    blinkInternalLed(LED_BLUE, 2, 120);
  }

  Wire.begin();

  pinMode(BAT_PIN, INPUT);
  analogReadResolution(12);

#ifdef PIN_CHARGING_CURRENT
  pinMode(PIN_CHARGING_CURRENT, OUTPUT);
  digitalWrite(PIN_CHARGING_CURRENT, LOW);
#endif

  pinMode(LED_BLUE_PIN, OUTPUT);
  pinMode(LED_RED_PIN, OUTPUT);
  pinMode(LED_GREEN_PIN, OUTPUT);
  ledsOff();

  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);

  batteryMv = measureBatteryMv();

  if (!BLE.begin()) {
    setLedLowBattery();
    while (1) {
      delay(100);
    }
  }

  BLE.setLocalName("WinkelSensor");
  BLE.setDeviceName("WinkelSensor");

  evaluatePowerState(true);
}

void loop() {
  BLE.poll();

  if (handleSleepButton()) {
    return;
  }

  if (currentMode == MODE_NORMAL) {
    updatePitchFilter();

    uint32_t now = millis();

    if ((int32_t)(now - nextBattMs) >= 0) {
      batteryMv = measureBatteryMv();
      nextBattMs = now + BATT_MEASURE_MS;

      bool usb = usbPresent();
      if ((!usb && batteryMv <= LOW_BATT_SLEEP_MV) || (usb && batteryMv < RESUME_BATT_MV)) {
        evaluatePowerState(true);
        return;
      }

      if (!usb && batteryMv <= LOW_BATT_LED_MV) {
        setLedLowBattery();
      } else {
        setLedNormal();
      }
    }

    if ((int32_t)(now - nextSendMs) >= 0) {
      advertisePacket(degToDdeg(smoothPitchDeg), makeFlags(usbPresent()));

      do {
        nextSendMs += SEND_MS;
      } while ((int32_t)(now - nextSendMs) >= 0);
    }

    delay(2);
    return;
  }

  if (currentMode == MODE_CHARGE_WAIT) {
    uint32_t now = millis();

    if ((int32_t)(now - nextChargeCheckMs) >= 0) {
      batteryMv = measureBatteryMv();
      nextChargeCheckMs = now + CHARGE_WAIT_CHECK_MS;

      bool usb = usbPresent();

      if (!usb && batteryMv <= LOW_BATT_SLEEP_MV) {
        enterLowBatterySystemOff();
        return;
      }

      if (usb && batteryMv >= RESUME_BATT_MV) {
        switchToNormal();
        return;
      }

      setLedCharging();
      advertisePacket(0, makeFlags(usb));
    }

    delay(20);
  }
}

I’m getting 43ua. Sleep now… :shushing_face: Don’t wake it. :grin: :v:


Try this code with the FIX and LMK.. :crossed_fingers:

HTH
GL :slight_smile: PJ :v:

Sleep_Current_Report_Enhanced.zip (237.0 KB)

also I’m testing a pushbutton wake too, (D5), a little later.

1 Like