adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.04k stars 1.19k forks source link

Third sleep option: RAM-preserving "deep" sleep #9521

Open Sola85 opened 1 month ago

Sola85 commented 1 month ago

CircuitPython currently supports two sleep modes: Light sleep and deep sleep. Given the specification of the two existing sleep modes, and the capabilities of many microcontrollers supported by CP, I think there is room for one more sleep mode, which would have a clear benefit. Here is my rationale:

But (as far as i can tell) all non-esp microcontrollers that are supported by CP and have the alarm module implemented, have options to preserve RAM contents during deep sleep at the cost of at most a few microamps. Some ports even preserve RAM contents during deep sleep today, but add an artificial reset upon wake. Below is an attempt to summarize the current state of CP regarding this topic.

The benefit would be instant wake, since python code would not have to be re-interpreted after wake. IMO, slow start and wake times are one of the most annoying drawbacks of micropython/circuitpython, and this could be solved by the proposed change.

There are a few options for how the API could look, e.g.:

Current state of CP and proposed behaviour in the new sleep mode (for all ports that currently implement the alarm module):

Port Current deep sleep behaviour Proposed behaviour in new sleep mode Estimated increase in sleep current over deep-sleep (according to datasheet)
nordic artificial reset after wake same as current deep sleep, but skip artificial reset 0
stm HAL_PWR_EnterSTANDYMode() HAL_PWR_EnterSTOPMode() 2-5µA -> 9-100 µA (depending on exact config)
raspberrypi* artificial reset after wake same as current deep sleep, but skip artificial reset 0
atmel-samd PM_SLEEPCFG_SLEEPMODE_BACKUP PM_SLEEPCFG_SLEEPMODE_STANDBY 2-10µA -> 22-50µA (depending on exact config)
espressif esp_deep_sleep_start() esp_light_sleep_start() or NotImplementedError

If there is interest in such a feature, I would be willling to implement it (at least for nordic, raspi and esp, as those are the ones I'm personally interested in).

*Note: I think the new RP2350 series also has low power ram retaining sleep, and could therefore also benefit from this feature.

Similar Issue: https://github.com/adafruit/circuitpython/issues/8771

bablokb commented 1 month ago

Have you tested anything of this? For a POC, this could be tested without an API change just to see if it is worth it. If you have any code to test, I would be happy to give it a try.

As a side note: there are currently a few issues open regarding sleep-modes (e.g. #9464 or #8977) so IMHO it would make more sense to fix the existing issues first with the existing sleep-modes before comparing results with a new third one.

dhalbert commented 1 month ago

The original light-sleep/deep-sleep distinction was based on a misinterpretation of what was preserved in Espressif light sleep. In retrospect, we might have just dropped light sleep. As you mentioned, "Light sleep promises to keep background tasks such as WIFI, BLE, audio playback, etc alive.", which is not necessarily all that useful. So redefining light sleep as RAM-preserving, but not activity-preserving, might be worthwhile. I am not sure that many people even use light sleep now, as it's not that useful.

bill88t commented 1 month ago

I also agree with this proposal. A simple blocking statement that does whatever it can in each port to lower the power usage and only monitoring an interrupt pin or alarm. In many applications, you may end up waiting for hours, but then have to respond immediately.

tannewt commented 1 month ago

My concern with making a light sleep that doesn't preserve activity is that some object state won't be preserved across the light sleep. This will be confusing and it won't be clear what is impacted.

If light sleep was smarter, then user code could shut down wifi and audio itself before sleeping and then restart it afterwards. That'd be clearer and fit in the existing API.

bablokb commented 1 month ago

I am not sure that many people even use light sleep now, as it's not that useful.

@dhalbert : I am using it in all my battery-based projects, at least for RP2040-based boards replacing time.sleep() with light-sleep. It saves about 30%.

Light-sleep is currently not fully activity preserving. One thing I noticed is that driving a ST7789 with backlight set to 0.7 fails during light-sleep, presumably because of the PWM not working as expected.

Sola85 commented 3 weeks ago

@tannewt If I understand correctly, your preferred solution would be such that light sleep automatically determines the most power conserving, ram retaining sleep mode that is possible, given the ongoing background tasks. E.g. on an esp32, if nothing is going on in the background, actual light sleep could be used and if WiFi is still active the current behaviour would be used. Is that correct?

As far as I can tell, this would be a valid solution of this issue. However it does sound like quite an effort to implement. I assume the alarm module would have to know about wifi, ble, SPI, I2C, ... and determine which of these are active and which are not. For external sensors, what should define an "ongoing activity"? Should all I2C and spi buses be .deinit()ed for the sleep logic to determine that it's safe to go to a deeper sleep mode? Or is there a less strict rule?

tannewt commented 3 weeks ago

Is that correct?

Yup!

However it does sound like quite an effort to implement. I assume the alarm module would have to know about wifi, ble, SPI, I2C, ... and determine which of these are active and which are not.

It isn't easy to implement but gives a consistent API across all ports. I'd ignore this complexity first though and just make sure you are getting the power savings you are hoping for. Once you confirm that, then add in things like wifi and ble.

You'll probably want a function call prevent_light_sleep() and allow_light_sleep() that you can call from the places that would be broken by a light sleep. They can each pass in a bitmap value for their "lock" and then you can sleep if the overall value is 0.

For external sensors, what should define an "ongoing activity"? Should all I2C and spi buses be .deinit()ed for the sleep logic to determine that it's safe to go to a deeper sleep mode? Or is there a less strict rule?

I think it's a matter of whether the light sleep will cause the used peripheral to lose state. If it does, then it should be deinit first by user code so that it is reinit after sleep. Or you could do this automatically.

I2C and SPI APIs are blocking so I bet they are fine. pulseio, countio, frequencyio all take data in asynchronously so they'd need to be disabled to sleep if the peripheral is turned off.

Sola85 commented 3 weeks ago

Makes sense 👍

One last thought: While this ensures a consistent API, the actual power consumption achieved by light sleep would become somewhat inconsistent (entering light sleep at different points in the code might result in different power consumptions). And forgetting to deinit a certain peripheral could result in an unexpected higher power consumption. Debugging this could become rather challenging. But this issue could maybe be solved by adding a new optional flag argument to light_sleep_until_alarms() which when enabled triggers an exception if some sleep-preventing peripheral is still enabled (instead of silently switching to a less power conserving sleep mode). This could make debugging the "smart light sleep" logic a lot easier.

tannewt commented 2 weeks ago

And forgetting to deinit a certain peripheral could result in an unexpected higher power consumption. Debugging this could become rather challenging.

Ya, I think this is a good point. However, I'm not exactly sure how to define when to raise an exception or warning. I always think that folks optimizing power should use a PPK2 to check they are getting the consumption they need. Lots of things on a board (like neopixels) can have a big impact too.

Maybe we could add an attribute of "expected light sleep current" that could indicate how low it thinks we can go. Or may an "active power users" list. Some way to know what will impact power without raising an error or warning. That's even more tracking and API though.

bill88t commented 2 weeks ago

There could be a verbose/debug bool option that before actually entering sleep lists the things that will be disabled and the estimated current use.

alarms.suspend_until(alarms=myalarm, verbose=True)
I2C: suspended
Wi-Fi: powered off
spi: state stored
usb: cannot be suspended

Estimated microcontroller draw: 14mA

Entering sleep..

But it would be a bit of a pain to estimate the draw of each thing. I think the checklist would be good enough, since it'd tell the user what he can improve on.

tannewt commented 2 weeks ago

I like that idea but don't think the core code should do it. Storing strings is expensive and printing from within a library is weird. Instead, we could add state that python-level code could read to determine this.

bill88t commented 2 weeks ago

Yes, all we'd really need is some attr bools in the alarm module to implement this. alarm.will_suspend_xxxx or alarm.will_suspend.xxxx Then python code could do the heavy lifting.

Sola85 commented 2 weeks ago

Or may an "active power users" list.

This sounds pretty straightforward. Essentially this would be a python API that internally just accesses the bitmap used in prevent_light_sleep() that you mentioned earlier.

Yes, all we'd really need is some attr bools in the alarm module to implement this. alarm.will_suspend_xxxx or alarm.will_suspend.xxxx

I'm not sure I understand the idea. To me, e.g. alarms.will_suspend.wifi sounds like a constant, that indicates whether wifi would be suspended during sleep. Is this what you meant?

bill88t commented 2 weeks ago

alarms.will_suspend.wifi == True means thar wifi will be suspended, yea. Not a constant. If wifi is connected for example, it could turn False indicating it'll not be suspended.

tannewt commented 1 week ago

This sounds pretty straightforward. Essentially this would be a python API that internally just accesses the bitmap used in prevent_light_sleep() that you mentioned earlier.

Yup, then you can introspect relevant state. A library can even verify sleep for you if needed.