ArduinoLowPower
ArduinoLowPower copied to clipboard
Making Low Power great again
After days of work (I am a noob), I have finally achieved the following:
- Deep sleep of the SAMD21G18
- Sleep current at around 3µA
- Wake up from sleep with external interrupt (button press) WITH edge detection (rising, falling or both edges)
- No floating pins, therefore no unnecessary current draw during sleep (hundreds of microamps!)
- no breaking of Arduino IO handling, all the pins are still in default (INPUT) state on initialization and during program execution
The issues:
- The SAMD21G18 can only wake up with edge detection, if the external interrupt controller (EIC) is being clocked during sleep. For some reason, the LowPower library does not do this, although there is a OSCULP32K oscillator inside the MCU which runs all the time anyway and no matter what (even in the deepest of sleep modes).
- The deep sleep current varies extremely if the pins are floating, which is the case for ALL pins by default with Arduino (all are set to INPUT at initialization). You'll see anything from 100µA to 500µA just because of the floating pins. Of course, one could just disconnect all the pins but with that there come two problems: 1) you can't wake up the MCU by bringing a pin low (i.e. pressing a button), and 2) all the pin config is lost (which pins are output, which are input, which are pulled up or down and which are muxed to peripherals) if you don't bother to save the registers somewhere first.
- It is possible to disable the functionality that the Arduino core puts all pins into INPUT mode on initialization but apparently that breaks stuff
- UART RX pins as well as the MISO pin are INPUT pins so I had to configure them as input pullups (for MISO, at least during sleep!)
So, what I have done to achieve my goals (actually not too complex):
In Setup(), first thing to do for me was to attach OCSULP32K to GCLK2:
GCLK->GENCTRL.reg = (GCLK_GENCTRL_GENEN | GCLK_GENCTRL_SRC_OSCULP32K | GCLK_GENCTRL_ID(2) | GCLK_GENCTRL_DIVSEL );
while (GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY)
Then setup your pins according to your needs, I had some buttons connected:
pinMode(BTN2, INPUT_PULLUP);
pinMode(BTN3, INPUT_PULLUP);
pinMode(BTN4, INPUT_PULLUP);
Set the UART RX pins to INPUT_PULLUP as well
pinMode(0, INPUT_PULLUP);
pinMode(31, INPUT_PULLUP);
Attach my interrupt with an ISR on falling edge:
attachInterrupt(this->ButtonPin, myISR, FALLING);
Now comes the fat part, I wrote a sleep function which 1) saves all the pin configs 2) sets all pins which currently are INPUTs to INPUT_PULLUPs 3) sets MISO to INPUT_PULLUP (there might be more pins for which this should be done) and which puts the MCU to sleep and then after wakeup restores all the registers so the user program isn't affected by misconfigured GPIO pins.
// Configure External Interrupt Controller (EIC) to GCLK2 which has been connected to OSCULP32K above, which in turn is ALWAYS running, even in deep sleep (can't be turned off)
// It seems that this MUST happen AFTER the interrupt has been attached
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) {}
USBDevice.detach();
USBDevice.end();
USBDevice.standby();
Serial.println();
for (uint8_t n = 0; n < PORT_GROUPS; n++)
{
regDir[n] = PORT->Group[n].DIR.reg;
regOut[n] = PORT->Group[n].OUT.reg;
}
// Save current pin config and set all INPUT pins to INPUT_PULLUP
for (uint8_t n=0; n<NUM_DIGITAL_PINS; n++)
{
this->regPinCFG[n] = PORT->Group[g_APinDescription[n].ulPort].PINCFG[g_APinDescription[n].ulPin].reg;
if ((PORT->Group[g_APinDescription[n].ulPort].PINCFG[g_APinDescription[n].ulPin].reg & (uint8_t)(PORT_PINCFG_INEN)) == (uint8_t)(PORT_PINCFG_INEN) &&
(PORT->Group[g_APinDescription[n].ulPort].DIR.reg & (uint32_t)(1<<g_APinDescription[n].ulPin)) == 0 &&
(PORT->Group[g_APinDescription[n].ulPort].PINCFG[g_APinDescription[n].ulPin].reg & (uint8_t)(PORT_PINCFG_PULLEN)) == 0)
{
pinMode(n, INPUT_PULLUP);
}
}
// Save MISO pin config
this->pinSPICFG[0] = PORT->Group[g_APinDescription[PIN_SPI_MISO].ulPort].PINCFG[g_APinDescription[PIN_SPI_MISO].ulPin].reg;
// Set MISO pin to non-floating states
pinMode(PIN_SPI_MISO, INPUT_PULLUP);
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; //cth to fix hangs
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
__DSB();
__WFI();
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk; //restore
for (uint8_t n = 0; n < PORT_GROUPS; n++)
{
PORT->Group[n].DIR.reg = regDir[n];
PORT->Group[n].OUT.reg = regOut[n];
}
// Restore previously saved pin config
for (uint8_t n=0; n<NUM_DIGITAL_PINS; n++)
{
PORT->Group[g_APinDescription[n].ulPort].PINCFG[g_APinDescription[n].ulPin].reg = this->regPinCFG[n];
}
// Restore previously saved MISO pin config
PORT->Group[g_APinDescription[PIN_SPI_MISO].ulPort].PINCFG[g_APinDescription[PIN_SPI_MISO].ulPin].reg = this->pinSPICFG[0];
USBDevice.init();
USBDevice.attach();
Most probably, to reach library quality and broad compatibility with all the possible configurations and SAMD21 boards, this needs to be refined very much but as I said: I am a noob and this took me days to find out and to do.
I would be very happy to see the fixes refined and implemented into the LowPower library because in the current state, with floating pins, only level detection external interrupts and so on, in my humble opinion this is a little bit useless for real low power applications.
I mean, the SAMD21 is a way more powerful MCU than the AVR (328p for example) and still it can achieve way lower standby power consumption and I think that it is a pity that this potential is kind of wasted.
Thank you for your attention.
Nice work mamama1. I found your comment interesting:
// Configure External Interrupt Controller (EIC) to GCLK2 which has been connected to OSCULP32K above, which in turn is ALWAYS running, even in deep sleep (can't be turned off) // It seems that this MUST happen AFTER the interrupt has been attached
I think the reason that you have to do this after the interrupt has been attached is
that attaching an interrupt calls the broken function configGCLK6()
which
corrupts the most recently configured clock. If you've just configured GCLK2
before attaching the interrupt, GCLK2
gets corrupted.
See issue #30.
I recommend patching my suggested fix in issue #30 if you're going to use this library.
I recommend forking this project. It's been like 3 years since your issue @denisbaylor and still it's not merged.
Deep sleep is one the THE MOST IMPORTANT aspect of MCU's since IoT is comming and comming fast, yet one of the less supported.
If any of you guys are willing to fork this library or make a sleep library from scratch for SAMD21 let me know, I would love to work with you guys!
@mamama1 I would love to see your entire code base. I have a small project going on where I could use your example.
seems like SAMD21 development is kind of dead. I haven't touched anything since 1,5 years ago and I can hardly remember anything. I'm sorry, but I think I've given away everything in my first post. I won't get better from there.
From your first post I don't know who is this
and who is regDir
. That is why I am asking. You've created an external class and just initiated it in setup?
Any idea why is SAMD21 dead?