From fa71703f5859bc26867e782a89821e3921fcf81d Mon Sep 17 00:00:00 2001 From: Simon Knopp Date: Tue, 18 Feb 2020 14:59:44 +1300 Subject: [PATCH] Add support for ADC wakeup interrupt on SAMD21 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. --- examples/AdcWakeup/AdcWakeup.ino | 61 ++++++++++++++ src/ArduinoLowPower.h | 17 ++++ src/samd/ArduinoLowPower.cpp | 140 +++++++++++++++++++++++++++---- 3 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 examples/AdcWakeup/AdcWakeup.ino diff --git a/examples/AdcWakeup/AdcWakeup.ino b/examples/AdcWakeup/AdcWakeup.ino new file mode 100644 index 0000000..24cafe2 --- /dev/null +++ b/examples/AdcWakeup/AdcWakeup.ino @@ -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 ++; +} \ No newline at end of file diff --git a/src/ArduinoLowPower.h b/src/ArduinoLowPower.h index 474c879..6863349 100644 --- a/src/ArduinoLowPower.h +++ b/src/ArduinoLowPower.h @@ -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: @@ -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); diff --git a/src/samd/ArduinoLowPower.cpp b/src/samd/ArduinoLowPower.cpp index a775137..fb8646f 100644 --- a/src/samd/ArduinoLowPower.cpp +++ b/src/samd/ArduinoLowPower.cpp @@ -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; @@ -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;