I am making a battery powered sensor that has to do some analogReads and send some SPI data every 50 ms or so. After that it can go back to a low power mode until the next RTC interrupt in 50 ms.
The problem I am having is the "sleep" between reads. Calling LowPower.sleep(50) actually takes about 1000 ms to wake up. That's some seriously deep sleep happening there -- something more like hibernation! It does result in a reasonably low 29 uA on my XIAO board with power LED removed while sleeping, but not useful if the shortest sleep is 1000 ms.
I then tried the ArduinoLowPower library version of LowPower.idle() but it actually only "sleeps" for about 1 ms and even then still consumes almost 9 mA so also not very helpful.
I also tried the Rocketscream library version of LowPower.idle() using IDLE_2 mode and it also only "sleeps" for about 1 ms so I need to loop it 50 times. It does reduce the power by a bit more, but it is still over 3 mA and too high for my application. Maybe waking and sleeping 50 times is artificially increasing the "sleep" power. Is there some way to prevent it from waking every millisecond?
Alternatively, reading a couple of analog inputs and sending a few bytes over SPI 20 times per second is pretty trivial work and doesn't need a 48 MHz MCU. Is it possible to slow down the SAMD instead of trying to get it to sleep? I did try creating a new board that had a F_CPU of 8000000L, and it seemed to have a great effect on the power, starting at just 0.5 mA, but sadly the sketch itself never ran. I tried a few other speeds (1 MHz and 24 MHz and 48 MHz) but it only ran the sketch at 48 MHz so clearly there is something dependent on F_CPU being 48000000L.
Any other ideas on how I can get current down to the very low mA, or ideally even uA, range? Unfortunately I am really struggling with getting low power even though past experience with 8 MHz Pro Minis made me think this would be easy. Unfortunately for this project I need USB connectivity so the ATmega328 isn't an option here.
Update: with some more reading and testing I discovered that wrapping the idle call as below will disable the 1 ms systick interrupt and thus an arbitrarily long RTC alarm can be used to resume from idle in the desired amount of time:
Unfortunately, that saved less than 2% of the current compared to just wrapping the idle call in a for loop repeating for as many milliseconds as desired, so really no good reason to do it that way.
Still hoping someone might have some ideas for dropping current to the uA range for ~50 ms at a time.
Here's how to switch generic clock 0 (GCLK0) or main CPU clock from the 48MHz Digital Frequency Locked Loop (DFLL48M) to the the 32.768kHz external crystal clock source (XOSC32K), using a "blink" sketch example:
// Switch generic clock 0 (GCLK0) over to the 32.768kHz external crystal clock source
void setup()
{
GCLK->GENCTRL.reg = //GCLK_GENCTRL_OE | // Test: enable GCLK output (on a selected pin)
GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // Enable GCLK0
GCLK_GENCTRL_SRC_XOSC32K | // Set the external 32.768kHz clock source (XOSC32K)
GCLK_GENCTRL_ID(0); // Select GCLK0
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
// Test: enable the GCLK0 output on D2 (PA14)
//PORT->Group[g_APinDescription[2].ulPort].PINCFG[g_APinDescription[2].ulPin].bit.PMUXEN = 1;
//PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg |= PORT_PMUX_PMUXE_H;
pinMode(LED_BUILTIN, OUTPUT); // Configure the built-in LED pin as an OUTPUT
}
void loop()
{
digitalWrite(LED_BUILTIN, HIGH); // Switch the built-in LED on
delay(10); // Wait for (48MHz / 32.768kHz) * 10ms = 14.65 seconds
digitalWrite(LED_BUILTIN, LOW); // Switch the built-in LED off
delay(10); // Wait for 14.65 seconds
}
As a test, I've routed the GCLK0 output to pin D2 (PA14) and can confirm that this outputs a 32.768kHz square wave on my scope.
Note that changing the main processor clock will affect the Arduino delay(), delayMicroseconds(), millis() and micros() timing functions. In the above example, the sketch should delay for 14.6 seconds instead of 10ms, as the CPU is running 1464 times slower.
By the way, I'm referring to pin D2 (PA14) on the Arduino Zero. On the Xiao, GCLK0 can be broken out on the SWCLK pad on the back side of the board, by changing the output enable lines to:
// Test: enable the Xiao GCLK0 output on the SWCLK pad (PA30)
PORT->Group[PORTA].PINCFG[30].bit.PMUXEN = 1;
PORT->Group[PORTA].PMUX[30 >> 1].reg |= PORT_PMUX_PMUXE_H;
That definitely has a big impact on power: active current went to something like 600 uA.
Unfortunately, it seems like too much of a good thing. Timing-critical things, like USB, stopped working altogether with the host reporting:
kernel: [850163.703720] usb 1-3.1.4: device descriptor read/64, error -32
Also, a loop of the sketch that used to take a few milliseconds now takes a few seconds, which is too slow for my purposes. Based on the loop timing I probably need the CPU to run at something like 2 to 4 MHz to allow the loop to complete but still take less power.
But that got me thinking: what about using OSC8M:
GCLK_GENCTRL_SRC_OSC8M | // Set the internal 8 MHz clock source (OSC8M)
Turns out it works similarly. CPU now runs at 1/6th the default speed which is more like what I need. Of course power went up too, with active current now at 2.8 mA. Still not bad considering at 48 MHz it was about 13 mA
As expected, USB still doesn't work, and that defeats a primary purpose of using a SAMD21 in the first place. That leaves me with a follow-on question: is it straightforward to re-calibrate the timers to account for the slower CPU speed such that timer based things (including USB) can work? I did try uploading the sketch using a custom board with an F_CPU of 8000000 but that still fails to run.
However, this still keeps the DFLL running at 48MHz, which sips more current.
Unfortunately, projects that require putting the microcontroller to sleep will kill any native USB connection, unlike the Arduino Uno/Nano that instead use a separate co-processor or FTDI chip that can keep the connection alive. That said, it's still possible keep a connection alive and view console, by using the SAMD21's Serial1 (UART) port, together with an external +3.3V Serial-To-USB (FTDI) converter.
Changing the CPU main clock will also upset all the timing, as the Arduino core code is built around the assumption that the processor and most peripherals will be running at 48MHz.
Thanks again! Actually, that clock divider is probably what I need. I realize that you can't leave USB connected when sleeping, but my workaround is to connect USB on boot, to allow downloading or sending messages to the MCU, and then after a certain amount of time with no connection to start low power modes. As long as I can turn up and down the clock speed as needed (ex: to connect USB), and that SPI keeps working well at the lower clock rate (I can't see a reason it wouldn't) then I think this could be my solution.
Update: the clock divider solution is working! Communication with timing sensitive devices doesn't work with the reduced CPU speed but the solution was some code to build a CPU governor, turning the speed up when transmitting data to devices, then turning it back down when finished. That works well for my application where little time is spent transmitting. The XIAO supply current with a divider of 12 (4 MHz) is 1.8 mA and for a divider of 6 (8 MHz) is 2.8 mA, which is adequate for my application. Hopefully someone else will find this helpful.