Boron LTE-M Cat1 - High current consumption in ULTRA_LOW_POWER mode

I’m using the Boron LTE-M Cat1 variant with an external sim card (deviceOS 2.2.0). Following the firmware code of AN028 and AN029, I’m putting the Boron in ULTRA_LOW_POWER mode. Upon wake-up from a timer alarm, I do a reset to the device.

The problem I’m facing is that the current consumption is too high in sleep mode. I’m measuring around 97 mA. For me that probably means that a high consumer is on, such as the modem. However, as you can see in the code below, I’m disconnecting from the network and turning-off the modem before going to sleep.

Nothing is connected to the Boron, except an external power supply at 3.3V to the Li+ pin.

Any suggestions of what to try or any ideas about why is it happening?

/*
 * Project DHI_WRM_PoCDemo_VegetationCamera
 * Description:
 * Author: NIAG
 * Date: 27-10-2021
 */

#include <Particle.h>

// Don't wait for cloud connection to run the code
SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(SEMI_AUTOMATIC);


// This is the maximum amount of time to wait for the cloud to be connected in
// milliseconds. This should be at least 5 minutes. If you set this limit shorter,
// on Gen 2 devices the modem may not get power cycled which may help with reconnection.
const std::chrono::milliseconds connectMaxTime = 6min;

// This is the minimum amount of time to stay connected to the cloud. You can set this
// to zero and the device will sleep as fast as possible, however you may not get 
// firmware updates and device diagnostics won't go out all of the time. Setting this
// to 10 seconds is typically a good value to use for getting updates.
const std::chrono::milliseconds cloudMinTime = 10s;

// How long to sleep
const std::chrono::seconds sleepTimeNormal = 1min;//24h;
// How long to sleep (short duration, after error)
const std::chrono::seconds sleepTimeShort = 5min;

// Maximum amount of time to wait for a user firmware download in milliseconds
// before giving up and just going back to sleep
const std::chrono::milliseconds firmwareUpdateMaxTime = 5min;

// How often to publish device diagnostics (vitals). If you set this 
// equal to the sleep period they'll be sent on every connection, or you can set
// it higher to save data. For example, if you set it to 24 * 60 * 60 it would
// only publish once per day. 
const std::chrono::seconds diagnosticPublishTime = 24h;

// These are the states in the finite state machine, handled in loop()
enum State {
    STATE_WAIT_CONNECTED = 0,
    STATE_MQTT_WAIT_CONNECTED,
    STATE_CAPTURE_PHOTO,
    STATE_PUBLISH,
    STATE_PRE_SLEEP,
    STATE_SLEEP,
    STATE_WAIT_DISCONNECTED,
    STATE_FIRMWARE_UPDATE
};
State state = STATE_SLEEP;
unsigned long stateTime;
bool firmwareUpdateInProgress = false;
long lastFirmwareUpdateCheck = 0;
long lastDiagnosticsPublish = 0;

SystemSleepConfiguration sleepConfigNormal;
SystemSleepConfiguration sleepConfigShort;

enum SleepDuration {
    SLEEP_DURATION_NORMAL = 0,
    SLEEP_DURATION_SHORT
};
SleepDuration sleepDuration = SLEEP_DURATION_NORMAL;

// setup() runs once, when the device is first turned on.
void setup() {
   /**
   * Setup system sleep configuration
   * Wake-up source is set to RTC
   */
  sleepConfigNormal.mode(SystemSleepMode::ULTRA_LOW_POWER)
                   .duration(sleepTimeNormal);

  sleepConfigShort.mode(SystemSleepMode::ULTRA_LOW_POWER)
                   .duration(sleepTimeShort);

  // It's only necessary to turn cellular on and connect to the cloud. Stepping up
  // one layer at a time with Cellular.connect() and wait for Cellular.ready() can
  // be done but there's little advantage to doing so.
  Cellular.on();
  Particle.connect();
  stateTime = millis();
}

// loop() runs over and over again, as quickly as it can execute.
void loop() {

  switch(state) {
      case STATE_WAIT_CONNECTED:
          // Wait for the connection to the Particle cloud to complete
          if (Particle.connected()) {
//              Log.info("connected to the cloud in %lu ms", millis() - stateTime);
              // state = STATE_PUBLISH;
              // connect to the MQTT server (unique id by Time.now())
              mqttClient.connect("sparkclient_" + String(Time.now()),"wrm","WRM_mosquitto_2021");   
              state = STATE_CAPTURE_PHOTO;
              stateTime = millis(); 
          }
          else
          if (millis() - stateTime >= connectMaxTime.count()) {
              // Took too long to connect, go to sleep with shorter sleep period.
//              Log.info("failed to connect, going to sleep");
              sleepDuration = SLEEP_DURATION_SHORT;
              state = STATE_SLEEP;
          }
          break;
      
      case STATE_CAPTURE_PHOTO:
          // capturePhoto();
          delay(2000);
          state = STATE_PUBLISH;
		  
      case STATE_PUBLISH:
          // Read camera fifo in burst mode and send everything to the cloud
          //if(!read_fifo_burst(myCAM))
          //{
            // Error: Either too big or empty buffer
          //}

          //Clear the capture done flag
          //myCAM.clear_fifo_flag();

          //if (millis() - stateTime < cloudMinTime.count()) {
          //    Log.info("waiting %lu ms before sleeping", cloudMinTime.count() - (millis() - stateTime));
          //    state = STATE_PRE_SLEEP;
          //}
          //else {
          //    state = STATE_SLEEP;
          //}
          Particle.disconnect();
          state = STATE_WAIT_DISCONNECTED;
          break;

      case STATE_WAIT_DISCONNECTED:
          if(Particle.disconnected())
          {
              Cellular.off();
              while(Cellular.isOn())
              {
                  delay(500);
              }              
              state = STATE_SLEEP;
          }
          break;
      
      case STATE_SLEEP:
          delay(10000);

          // TODO: put camera to low-power mode
          
          // Go to sleep
          if (sleepDuration == SLEEP_DURATION_SHORT)
          {
//            Log.info("going to sleep for %ld seconds", (long) sleepTimeShort.count());
              System.sleep(sleepConfigShort);
          }
          else if (sleepDuration == SLEEP_DURATION_NORMAL)
          {
//            Log.info("going to sleep for %ld seconds", (long) sleepTimeNormal.count());
              System.sleep(sleepConfigNormal);
          }

//          Log.info("woke from sleep");

          // On wake-up, device will be reset
          System.reset();
          break;
}

@niagFT ,

Looks like a cool project and I wish you luck with it.

Current consumption of 97mA is a lot and it makes me think that perhaps your cellular modem is not being powered down.

In looking at your states, it seems that you connect (STATE_WAIT_CONNECTED), then capture a photo, then go to STATE_WAIT_DISCONNECTED. But, in that state, if you are still Particle.connected(), it seems you would skip turning off the cellular modem. I may be missing something but, I would have expected a Particle.disconnect() before that state.

Alos, you may want to consider a waitFor(Cellular.isOff, 30000) to prevent the possibility of execution hanging if the cellular modem stays on.

Hope this helps,

Chip

@chipmc

Thank you for your answer. You are of course correct, but unfortunately I’m doing that already. In my effort to delete commented/not relevant code for the post, I also skipped one State completely (the STATE_PUBLISH).

As you can see (I’ve just corrected the code), what I’m doing there, is indeed Particle.disconnect() and then go to the state where I wait until I’m disconnected… don’t you think that’s correct now?

And I agree with you, this ~100mA seems like the modem is still on.

Does that have anything to do with the older OS I’m using? I didn’t see though any relevant entry in the change logs from 2.2.0 to the latest.

Can it be related to the fact that I’m using a 3rd-party sim? I can see it is powered by the same rail (different pin though) as the esim.

Edit: Oh yes, and thanks for the tip for adding a timeout for the cellular off. That’s definitely a good idea.

@niagFT ,

Ok, wondered if you had taken care of that Particle.disconnect() elsewhere. Always a challenge to decide what code to put into the post…

It is possible that you may not have disconnected from Particle yet. Perhaps you could try belt and suspenders:

bool disconnectFromParticle()                                     // Ensures we disconnect cleanly from Particle
{
  Particle.disconnect();
  waitUntil(Particle.disconnected)
  return true;
}

Just to make sure you are disconnected before moving to the next state.

Chip