Wake up from deep sleep(Standby mode with DS3231)

Hi guys! I am trying to make a simple sensor node for a project of mine and I've stumbled upon this issue with the deep sleep or STANDBY mode.

For my hardware, I am using:

Board: Arduino MKR1310 WAN
RTC: DS3231 module

The SQW pin is connected to the INT7(pin 7) of the board.

#include <Arduino.h>
#include <Wire.h>
#include <RTC.h>
#include <SPI.h>

void ISR();
void setAlarm();
void deepSleep();
void onAlarm();

// the pin that is connected to SQW
#define CLOCK_INTERRUPT_PIN 7

bool alarmSet = false;

void setup() {
    Serial.begin(9600);

    // initializing the rtc
    if(!rtc.begin()) {
        Serial.println("Couldn't find RTC!");
        Serial.flush();
        while (1) delay(10);
    }

    if(rtc.lostPower()) {
        // this will adjust to the date and time at compilation
        rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    }

    //we don't need the 32K Pin, so disable it
    rtc.disable32K();

    // Making it so, that the alarm will trigger an interrupt
    pinMode(CLOCK_INTERRUPT_PIN, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(CLOCK_INTERRUPT_PIN), ISR, FALLING);

    // set alarm 1, 2 flag to false (so alarm 1, 2 didn't happen so far)
    // if not done, this easily leads to problems, as both register aren't reset on reboot/recompile
    rtc.clearAlarm(1);
    rtc.clearAlarm(2);

    // stop oscillating signals at SQW Pin
    // otherwise setAlarm1 will fail
    rtc.writeSqwPinMode(DS3231_OFF);

    // turn off alarm 2 (in case it isn't off already)
    // again, this isn't done at reboot, so a previously set alarm could easily go overlooked
    rtc.disableAlarm(2);

    // schedule an alarm 10 seconds in the future
}

void loop() {
    // print current time
    delay(10000);
    
    char date[10] = "hh:mm:ss";
    rtc.now().toString(date);
    Serial.print(date);

    // the stored alarm value + mode
    if(!alarmSet){
      setAlarm();
    }
    DateTime alarm1 = rtc.getAlarm1();
    Ds3231Alarm1Mode alarm1mode = rtc.getAlarm1Mode();
    char alarm1Date[12] = "DD hh:mm:ss";
    alarm1.toString(alarm1Date);
    Serial.print(" [Alarm1: ");
    Serial.print(alarm1Date);
    Serial.print(", Mode: ");
    switch (alarm1mode) {
      case DS3231_A1_PerSecond: Serial.print("PerSecond"); break;
      case DS3231_A1_Second: Serial.print("Second"); break;
      case DS3231_A1_Minute: Serial.print("Minute"); break;
      case DS3231_A1_Hour: Serial.print("Hour"); break;
      case DS3231_A1_Date: Serial.print("Date"); break;
      case DS3231_A1_Day: Serial.print("Day"); break;
    }
    Serial.println("]");
    deepSleep();
}


void setAlarm() {
  if(!rtc.setAlarm1(
            rtc.now() + TimeSpan(10),
            DS3231_A1_Second // this mode triggers the alarm when the seconds match. See Doxygen for other options
    )) {
        Serial.println("Error, alarm wasn't set!");
    }else {
        Serial.println("Alarm will happen in 10 seconds!");
        alarmSet = true;
    }
}

void deepSleep() {
  // Set the sleep mode to standby
  SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
  __DSB();
  // SAMD sleep and wait for interrupt;
  __WFI();
}

void ISR() {
  Serial.println("I WOKE UP!");
  rtc.clearAlarm(ALARM_1);
  alarmSet = false;
}

RTC.h

#include <RTClib.h>

#define ALARM_1 ((uint8_t)1)
#define ALARM_2 ((uint8_t)2)
#define EXT_INT ((uint8_t)4)

RTC_DS3231 rtc;

Here is the sketch that I am using. The thing is that if I remove the deepSleep function call, the alarm goes off and ISR function is called once every 10 seconds. But just using this sketch as it is, the controller goes to sleep and never wakes up.

What exactly am I doing wrong? I have no idea. Please help!

NOTE:

The big delay at the start of the loop is to be able to program the board without resetting it.

That has the SAMD21G18A processor, which is quite complex, and not completely supported by any Arduino core that I've encountered.

Deep sleep is an extremely important but poorly supported function, especially with external interrupt wakeups. I've been struggling with that on and off for weeks now and unfortunately, the number of Arduino-oriented people who understand the SAMD21 well enough to help solve the sleep problem seems to be close to zero.

Libraries to try include the ArduinoLowPower, ZeroRTC and Adafruit SleepyDog libraries, but they all have issues, so study the (minimal) docs and try the examples.

Interrupt on state change may not work with any of them, because a separate clock for the external interrupt controller (EIC) has to be properly implemented and set up for each interrupt pin.

   attachInterrupt(digitalPinToInterrupt(CLOCK_INTERRUPT_PIN), ISR, FALLING);

If you run into problems, carefully study the issues on the Github issue pages, as fixes are proposed that the library authors don't seem to be interested to implement.

Well...this unfortunate reality is kind of unwarranted. I mean, SAMD21 is way more performant then atmega328p and the MKR lineup is really cool.

I think that laziness and lack of determination to improve got us here. Thank you @jremington. If you find something useful, please let me know. I will update this topic with my findings.

Do try the libraries I've already mentioned.

They don't work for my project, so I'm currently investigating this very promising new SAMD21 sleep library. The eic_wakeup (callback) example works with the Adafruit Feather M0, but it will take a bit to integrate and test it in the project I'm working on.

I tried them and are not working at all :slight_smile: .

In this library, The ULONG_MAX, USHRT_MAX, and UCHAR_MAX are not defined in this library. Maybe they are defined with the Arduino IDE that we are not using. I am using PlatformIO because we are multiple people working on the same project.

To solve the issue:

System.h

#include <limits.h>

Another option maybe

That IDE is very rarely mentioned on this forum, so few would be able to offer help with it.

I don't need help with it. I need a way to make the standby-wake up cycle work :smiley:

I was able to get the ArduinoLowPower library wake from sleep example to work with interrupt on change, after implementing the fix mentioned in Github library issue #30, but it doesn't work for my project. Still debugging!

Since the level interrupt does not depend on the EIC clock, with the RTC you may have better luck with specifying a HIGH level mode in attachInterrupt().

Could you give me the links. I think I am using the wrong library

I finally got it! The problem with ArduinoLowPower or RTCZero is that they are corrupting the EIC internal clock.

Inside ArduinoLowPower lib

GCLK->CLKCTRL.bit.CLKEN = 0; //disable GCLK module 

So according to this post from Github, the way to fix this is to reset the GCLK(General Clock) to the EIC one.

Here is how

void set_EIC_general_clock()
{
  // Enable GCLK for EIC (External Interrupt Controller)
  GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK2 | GCLK_CLKCTRL_ID( GCLK_CLKCTRL_ID_EIC_Val ) );
  while (GCLK->STATUS.bit.SYNCBUSY);

}

That is not all. In order to work properly, you HAVE TO SET IT AFTER THE INTERRUPT ATTACHMENT!

I have no idea why. The only thing I was able to find is this line from the WInterrupts.c file:

static void __initialize()
{
  memset(ISRlist,     0, sizeof(ISRlist));
  memset(ISRcallback, 0, sizeof(ISRcallback));
  nints = 0;

  NVIC_DisableIRQ(EIC_IRQn);
  NVIC_ClearPendingIRQ(EIC_IRQn);
  NVIC_SetPriority(EIC_IRQn, 0);
  NVIC_EnableIRQ(EIC_IRQn);

  // Enable GCLK for IEC (External Interrupt Controller) -> THIS ONE RIGHT HERE FUCKS IT
  GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCM_EIC));

/* Shall we do that?
  // Do a software reset on EIC
  EIC->CTRL.SWRST.bit = 1 ;
  while ((EIC->CTRL.SWRST.bit == 1) && (EIC->STATUS.SYNCBUSY.bit == 1)) { }
*/

  // Enable EIC
  EIC->CTRL.bit.ENABLE = 1;
  while (EIC->STATUS.bit.SYNCBUSY == 1) { }
}

Here is what my setup looks like:

void setup() {
  Serial.begin(9600);
  setup_ds3231();
  clear_alarms();
  enable_deep_sleep();
  attachInterrupt(digitalPinToInterrupt(EXT_INT), ISR, FALLING);
  set_EIC_general_clock();
}

void set_EIC_general_clock()
{
  // Enable GCLK for EIC (External Interrupt Controller)
  GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK2 | GCLK_CLKCTRL_ID( GCLK_CLKCTRL_ID_EIC_Val ) );
  while (GCLK->STATUS.bit.SYNCBUSY);

}

void ISR() {
  clearAlarm(ALARM_1);
  isAlarmSet = false;
  ISRCALLED++;
}


void enable_deep_sleep(){
  SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;// | SCB_SCR_SLEEPONEXIT_Msk;  // Put the SAMD21 in deep sleep upon executing the __WFI() function
}

void deep_sleep(){
  // USBDevice.detach();
  SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;           // Disable SysTick interrupts
  __DSB();                                              // Complete outstanding memory operations - not required for SAMD21 ARM Cortex M0+
  __WFI();                                              // Put the SAMD21 into deep sleep, Zzzzzzzz...
  SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;            // Enable SysTick interrupts
  // USBDevice.attach();
}

Give it a try and let me know.

I will investigate the github post linked above since he claimed he was able to get 3uA in deep sleep and I will update this post. Till then, keep me posted!

Here is my demo.

Thanks, that gives me something else to try.

I was able to get the eic_callback example in this recently published sleep library to work fine for wake-from-sleep upon state change, at least for Arduino pins D10 and D11, but not for pin D3, which is connected to the the radio interrupt (Adafruit Feather M0 LoRa) and is the only pin of interest to me.

So now I'm investigating whether there might be a board and/or library error associated with pin D3.

Perhaps not incidentally, the working example does set up the EIC clock after attaching the interrupt pin.

/**
 * wait for external interrupt of SAMD21 in standby mode
 * RISING and FALLING interrupts work as expected
 * - 2 µA standby current at 3.0 V excl. USB/LED
 */

#include <System.h>
using namespace SAMD21LPE;

#define EXT_INT_PIN 11

void externalInterruptHandler() {
}

void setupEIC() {
  noInterrupts();

  // enable PORT and EIC modules
  System::enablePORT();
  System::enableEIC();

  // enable external interrupt on pin
  pinMode(EXT_INT_PIN, INPUT_PULLUP);
  attachInterrupt(EXT_INT_PIN, externalInterruptHandler, FALLING);

  // configure low power clock generator to run at 1 kHz
  const byte GCLKGEN_ID_1K = 6;
  System::setupClockGenOSCULP32K(GCLKGEN_ID_1K, 4); // 2^(4+1) = 32 -> 1 kHz

  // change clock generator for EIC from 0 (DFLL48M, assigned by attachInterrupt) to 6 (OSCULP32K), which stays enabled in standby (required for input edge detection)
  System::enableClock(GCM_EIC, GCLKGEN_ID_1K);

  interrupts();
}

void setup() {
  // disable non essential MCU modules (incl. USB, PORT and EIC)
  System::reducePowerConsumption();
  pinMode(LED_BUILTIN,OUTPUT);
  
  // setup external interrupt controller
  setupEIC();

  System::setSleepOnExitISR(false);
  System::setSleepMode(System::STANDBY);
}

void loop() {
digitalWrite(LED_BUILTIN,1); //blink LED then sleep, repeat on external interrupt
delay(200);
digitalWrite(LED_BUILTIN,0);
System::sleep();
}

BTW I don't have access to Arduino pin D3 to test the above example on the Feather M0, so if you do have access to it on the MKR1310 and can test it for "digital pin 3", it might be of great help. Note that the Adafruit and Arduino definitions assigning a processor pin to D3 might be different.

Pin is D3_IRQ or PA09 on the schematic snip for the Feather M0, below:

image

To go above interrupt channel 7, it's necessary to use the CONFIG[1] register. For example to use EIC interrupt channel 8:

EIC->CONFIG[1].reg |= EIC_CONFIG_SENSE0_RISE;                            // Set event detecting a RISE-ing edge

channel 9:

EIC->CONFIG[1].reg |= EIC_CONFIG_SENSE1_RISE;                            // Set event detecting a RISE-ing edge

...and so on...

If the channel is below or equal to 7, then use CONFIG[0]

So check the channel of the interrupt.

Thanks. It appears that the Adafruit code for attachInterrupt() selects the appropriate EIC->CONFIG[] register, so that is not the problem.

I also determined that in my tests, I had correctly assigned the EIC clock after attachInterrupt() was executed, so that is not the problem either.

I've ordered a different SAMD21 board, where D3 is exposed, for hardware tests.

From the Adafruit core function Winterrupt.c

/*
 * \brief Specifies a named Interrupt Service Routine (ISR) to call when an interrupt occurs.
 *        Replaces any previous function that was attached to the interrupt.
 */
void attachInterrupt(uint32_t pin, voidFuncPtr callback, uint32_t mode)
{
	static int enabled = 0;
	uint32_t config;
	uint32_t pos;

	#if ARDUINO_SAMD_VARIANT_COMPLIANCE >= 10606
	EExt_Interrupts in = g_APinDescription[pin].ulExtInt;
	#else
	EExt_Interrupts in = digitalPinToInterrupt(pin);
	#endif
	if (in == NOT_AN_INTERRUPT) return;

	if (!enabled) {
		__initialize();
		enabled = 1;
	}
	uint32_t inMask = (1UL << in);
	// Enable wakeup capability on pin in case being used during sleep
//	#if defined(__SAMD51__)
	//I believe this is done automatically
//	#else
	EIC->WAKEUP.reg |= (1 << in);
//	#endif

	// Only store when there is really an ISR to call.
	// This allow for calling attachInterrupt(pin, NULL, mode), we set up all needed register
	// but won't service the interrupt, this way we also don't need to check it inside the ISR.
	if (callback)
	{
		if (in == EXTERNAL_INT_NMI) {
			EIC->NMIFLAG.bit.NMI = 1; // Clear flag
			switch (mode) {
			  case LOW:
				EIC->NMICTRL.bit.NMISENSE = EIC_NMICTRL_NMISENSE_LOW;
				break;

			  case HIGH:
				EIC->NMICTRL.bit.NMISENSE = EIC_NMICTRL_NMISENSE_HIGH;
				break;

			  case CHANGE:
				EIC->NMICTRL.bit.NMISENSE = EIC_NMICTRL_NMISENSE_BOTH;
				break;

			  case FALLING:
				EIC->NMICTRL.bit.NMISENSE = EIC_NMICTRL_NMISENSE_FALL;
				break;

			  case RISING:
				EIC->NMICTRL.bit.NMISENSE = EIC_NMICTRL_NMISENSE_RISE;
				break;
			}

			// Assign callback to interrupt
			ISRcallback[EXTERNAL_INT_NMI] = callback;

		} else { // Not NMI, is external interrupt

			// Assign pin to EIC
			pinPeripheral(pin, PIO_EXTINT);

			// Store interrupts to service in order of when they were attached
			// to allow for first come first serve handler
			uint32_t current = 0;

			// Check if we already have this interrupt
			for (current=0; current<nints; current++) {
			  if (ISRlist[current] == inMask) {
				break;
			  }
			}
			if (current == nints) {
			  // Need to make a new entry
			  nints++;
			}
			ISRlist[current] = inMask;       // List of interrupt in order of when they were attached
			ISRcallback[current] = callback; // List of callback adresses

			// Look for right CONFIG register to be addressed
			if (in > EXTERNAL_INT_7) {
			  config = 1;
			  pos = (in - 8) << 2;
			} else {
			  config = 0;
			  pos = in << 2;
			}

			#if defined (__SAMD51__)
			EIC->CTRLA.bit.ENABLE = 0;
			while (EIC->SYNCBUSY.bit.ENABLE == 1) { }
			#endif

			EIC->CONFIG[config].reg &=~ (EIC_CONFIG_SENSE0_Msk << pos); // Reset sense mode, important when changing trigger mode during runtime
			switch (mode)
			{
			  case LOW:
				EIC->CONFIG[config].reg |= EIC_CONFIG_SENSE0_LOW_Val << pos;
				break;

			  case HIGH:
				EIC->CONFIG[config].reg |= EIC_CONFIG_SENSE0_HIGH_Val << pos;
				break;

			  case CHANGE:
				EIC->CONFIG[config].reg |= EIC_CONFIG_SENSE0_BOTH_Val << pos;
				break;

			  case FALLING:
				EIC->CONFIG[config].reg |= EIC_CONFIG_SENSE0_FALL_Val << pos;
				break;

			  case RISING:
				EIC->CONFIG[config].reg |= EIC_CONFIG_SENSE0_RISE_Val << pos;
				break;
			}
		}
		// Enable the interrupt
		EIC->INTENSET.reg = EIC_INTENSET_EXTINT(1 << in);
	}

	#if defined (__SAMD51__)
	EIC->CTRLA.bit.ENABLE = 1;
	while (EIC->SYNCBUSY.bit.ENABLE == 1) { }
	#endif
}

My solution is not good. The current reaches between 20mA to 40mA, which is a lot for a deep sleep current.

This might be a silly question, but why do you need an external RTC module if all you want to do is wake the processor periodically to read the sensor?

I have a project where I use the Adafruit SleepyDog library and the SAMD’s RTC function to wake the processor to take a reading and it works perfectly with a sleep current of approx 110uA.

I would be doubtful that you could get much lower while still powering the board from the 3.3V onboard regulator.