Skip to content

Commit

Permalink
Add support for ADC wakeup interrupt on SAMD21
Browse files Browse the repository at this point in the history
This can be used to configure the ADC window interrupt on the SAMD21. It
uses OSCULP32K via GCLK6 to clock the ADC while in sleep mode (the same
as used for the EIC).

Note that attachAdcInterrupt()/detachAdcInterrupt() should be called
immediately before/after LowPower.sleep() otherwise analogRead() will
not work as expected.

There is also an example (AdcWakeup.ino) which is much like the
ExternalWakeup example but uses the ADC interrupt instead.
  • Loading branch information
sijk committed Mar 1, 2020
1 parent c1b24fb commit fa71703
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 14 deletions.
61 changes: 61 additions & 0 deletions examples/AdcWakeup/AdcWakeup.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
AdcWakeup
This sketch demonstrates the usage of the ADC to wakeup a chip in sleep mode.
Sleep modes allow a significant drop in the power usage of a board while it does nothing waiting for an event to happen. Battery powered application can take advantage of these modes to enhance battery life significantly.
In this sketch, changing the voltage on pin A0 will wake up the board. You can test this by connecting a potentiometer between VCC, A0, and GND.
Please note that, if the processor is sleeping, a new sketch can't be uploaded. To overcome this, manually reset the board (usually with a single or double tap to the RESET button)
This example code is in the public domain.
*/

#include "ArduinoLowPower.h"

// Blink sequence number
// Declare it volatile since it's incremented inside an interrupt
volatile int repetitions = 1;

// Pin used to trigger a wakeup
const int pin = A0;
// How sensitive to be to changes in voltage
const int margin = 10;

void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(pin, INPUT);
}

void loop() {
for (int i = 0; i < repetitions; i++) {
digitalWrite(LED_BUILTIN, HIGH);
delay(500);
digitalWrite(LED_BUILTIN, LOW);
delay(500);
}

// Read the voltage at the ADC pin
int value = analogRead(pin);

// Define a window around that value
uint16_t lo = max(value - margin, 0);
uint16_t hi = min(value + margin, UINT16_MAX);

// Attach an ADC interrupt on pin A0, calling repetitionsIncrease when the voltage is outside the given range.
// This should be called immediately before LowPower.sleep() because it reconfigures the ADC internally.
LowPower.attachAdcInterrupt(pin, repetitionsIncrease, ADC_INT_OUTSIDE, lo, hi);

// Triggers an infinite sleep (the device will be woken up only by the registered wakeup sources)
// The power consumption of the chip will drop consistently
LowPower.sleep();

// Detach the ADC interrupt. This should be called immediately after LowPower.sleep() because it restores the ADC configuration after waking up.
LowPower.detachAdcInterrupt();
}

void repetitionsIncrease() {
// This function will be called once on device wakeup
// You can do some little operations here (like changing variables which will be used in the loop)
// Remember to avoid calling delay() and long running functions since this functions executes in interrupt context
repetitions ++;
}
17 changes: 17 additions & 0 deletions src/ArduinoLowPower.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ typedef enum{
ANALOG_COMPARATOR_WAKEUP = 3
} wakeup_reason;

#ifdef ARDUINO_ARCH_SAMD
enum adc_interrupt
{
ADC_INT_BETWEEN,
ADC_INT_OUTSIDE,
ADC_INT_ABOVE_MIN,
ADC_INT_BELOW_MAX,
};
#endif


class ArduinoLowPowerClass {
public:
Expand Down Expand Up @@ -68,10 +78,17 @@ class ArduinoLowPowerClass {
wakeup_reason wakeupReason();
#endif

#ifdef ARDUINO_ARCH_SAMD
void attachAdcInterrupt(uint32_t pin, voidFuncPtr callback, adc_interrupt mode, uint16_t lo, uint16_t hi);
void detachAdcInterrupt();
#endif

private:
void setAlarmIn(uint32_t millis);
#ifdef ARDUINO_ARCH_SAMD
RTCZero rtc;
voidFuncPtr adc_cb;
friend void ADC_Handler();
#endif
#ifdef BOARD_HAS_COMPANION_CHIP
void (*companionSleepCB)(bool);
Expand Down
140 changes: 126 additions & 14 deletions src/samd/ArduinoLowPower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
#include "ArduinoLowPower.h"
#include "WInterrupts.h"

static void configGCLK6()
{
// enable EIC clock
GCLK->CLKCTRL.bit.CLKEN = 0; //disable GCLK module
while (GCLK->STATUS.bit.SYNCBUSY);

GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK6 | GCLK_CLKCTRL_ID( GCM_EIC )) ; //EIC clock switched on GCLK6
while (GCLK->STATUS.bit.SYNCBUSY);

GCLK->GENCTRL.reg = (GCLK_GENCTRL_GENEN | GCLK_GENCTRL_SRC_OSCULP32K | GCLK_GENCTRL_ID(6)); //source for GCLK6 is OSCULP32K
while (GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY);

GCLK->GENCTRL.bit.RUNSTDBY = 1; //GCLK6 run standby
while (GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY);

/* Errata: Make sure that the Flash does not power all the way down
* when in sleep mode. */

NVMCTRL->CTRLB.bit.SLEEPPRM = NVMCTRL_CTRLB_SLEEPPRM_DISABLED_Val;
}

void ArduinoLowPowerClass::idle() {
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
PM->SLEEP.reg = 2;
Expand Down Expand Up @@ -80,26 +101,117 @@ void ArduinoLowPowerClass::attachInterruptWakeup(uint32_t pin, voidFuncPtr callb
//pinMode(pin, INPUT_PULLUP);
attachInterrupt(pin, callback, mode);

// enable EIC clock
GCLK->CLKCTRL.bit.CLKEN = 0; //disable GCLK module
while (GCLK->STATUS.bit.SYNCBUSY);
configGCLK6();

GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK6 | GCLK_CLKCTRL_ID( GCM_EIC )) ; //EIC clock switched on GCLK6
while (GCLK->STATUS.bit.SYNCBUSY);
// Enable wakeup capability on pin in case being used during sleep
EIC->WAKEUP.reg |= (1 << in);
}

GCLK->GENCTRL.reg = (GCLK_GENCTRL_GENEN | GCLK_GENCTRL_SRC_OSCULP32K | GCLK_GENCTRL_ID(6)); //source for GCLK6 is OSCULP32K
while (GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY);
void ArduinoLowPowerClass::attachAdcInterrupt(uint32_t pin, voidFuncPtr callback, adc_interrupt mode, uint16_t lo, uint16_t hi)
{
uint8_t winmode = 0;

GCLK->GENCTRL.bit.RUNSTDBY = 1; //GCLK6 run standby
while (GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY);
switch (mode) {
case ADC_INT_BETWEEN: winmode = ADC_WINCTRL_WINMODE_MODE3; break;
case ADC_INT_OUTSIDE: winmode = ADC_WINCTRL_WINMODE_MODE4; break;
case ADC_INT_ABOVE_MIN: winmode = ADC_WINCTRL_WINMODE_MODE1; break;
case ADC_INT_BELOW_MAX: winmode = ADC_WINCTRL_WINMODE_MODE2; break;
default: return;
}

// Enable wakeup capability on pin in case being used during sleep
EIC->WAKEUP.reg |= (1 << in);
adc_cb = callback;

/* Errata: Make sure that the Flash does not power all the way down
* when in sleep mode. */
configGCLK6();

NVMCTRL->CTRLB.bit.SLEEPPRM = NVMCTRL_CTRLB_SLEEPPRM_DISABLED_Val;
// Configure ADC to use GCLK6 (OSCULP32K)
while (GCLK->STATUS.bit.SYNCBUSY) {}
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID_ADC
| GCLK_CLKCTRL_GEN_GCLK6
| GCLK_CLKCTRL_CLKEN;
while (GCLK->STATUS.bit.SYNCBUSY) {}

// Set ADC prescaler as low as possible
ADC->CTRLB.bit.PRESCALER = ADC_CTRLB_PRESCALER_DIV4;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Configure window mode
ADC->WINLT.reg = lo;
ADC->WINUT.reg = hi;
ADC->WINCTRL.reg = winmode;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Enable window interrupt
ADC->INTENSET.bit.WINMON = 1;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Enable ADC in standby mode
ADC->CTRLA.bit.RUNSTDBY = 1;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Enable continuous conversions
ADC->CTRLB.bit.FREERUN = 1;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Configure input mux
ADC->INPUTCTRL.bit.MUXPOS = g_APinDescription[pin].ulADCChannelNumber;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Enable the ADC
ADC->CTRLA.bit.ENABLE = 1;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Start continuous conversions
ADC->SWTRIG.bit.START = 1;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Enable the ADC interrupt
NVIC_EnableIRQ(ADC_IRQn);
}

void ArduinoLowPowerClass::detachAdcInterrupt()
{
// Disable the ADC interrupt
NVIC_DisableIRQ(ADC_IRQn);

// Disable the ADC
ADC->CTRLA.bit.ENABLE = 0;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Disable continuous conversions
ADC->CTRLB.bit.FREERUN = 0;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Disable ADC in standby mode
ADC->CTRLA.bit.RUNSTDBY = 1;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Disable window interrupt
ADC->INTENCLR.bit.WINMON = 1;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Disable window mode
ADC->WINCTRL.reg = ADC_WINCTRL_WINMODE_DISABLE;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Restore ADC prescaler
ADC->CTRLB.bit.PRESCALER = ADC_CTRLB_PRESCALER_DIV512_Val;
while (ADC->STATUS.bit.SYNCBUSY) {}

// Restore ADC clock
while (GCLK->STATUS.bit.SYNCBUSY) {}
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID_ADC
| GCLK_CLKCTRL_GEN_GCLK0
| GCLK_CLKCTRL_CLKEN;
while (GCLK->STATUS.bit.SYNCBUSY) {}

adc_cb = nullptr;
}

void ADC_Handler()
{
// Clear the interrupt flag
ADC->INTFLAG.bit.WINMON = 1;
LowPower.adc_cb();
}

ArduinoLowPowerClass LowPower;
Expand Down

1 comment on commit fa71703

@aalbinati
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sijk this addition is great! Thanks for coding it.

I am having an error with it though. If I use it in conjunction with a timed interrupt, for example: LowPower.sleep(uint32_t(30000)); Then the device wakes up but never runs the loop. What am I missing? Can both interrupts exist at the same time?

Please sign in to comment.