Skip to content
This repository was archived by the owner on Sep 6, 2023. It is now read-only.

Deep Sleep #85

Closed
wants to merge 1 commit into from
Closed

Deep Sleep #85

wants to merge 1 commit into from

Conversation

MrSurly
Copy link
Contributor

@MrSurly MrSurly commented May 11, 2017

EXT0 wake:

import machine as m
p = m.Pin(4)
r = m.RTC()
p.init(p.IN, p.PULL_UP)
r.wake_on_ext0(pin = p, level = 0)
m.deepsleep()

EXT1 wake:

import machine as m
pins = (m.Pin(2), m.Pin(4))
for p in pins: 
    p.init(m.Pin.IN, m.Pin.PULL_UP)
r = m.RTC()
r.wake_on_ext1(pins = pins, level = r.WAKEUP_ALL_LOW)
m.deepsleep()

Timer wake:

import machine as m
m.deepsleep(sleep_ms = 3000)

Datetime:

import machine as m
r = m.rtc()
r.datetime() # Get: (1970, 1, 1, 5, 0, 0, 3, 62)
r.datetime((2017, 5, 17, None, 15, 21, 00, 128)) # set, weekday not used

@pacmac
Copy link

pacmac commented Jun 13, 2017

how can I merge this into my local rep so that I can compile and test it ?

@annejan
Copy link

annejan commented Jun 13, 2017

@pacmac git pull git@github.com:MrSurly/micropython-esp32.git dev-deepsleep is one way . .

@pacmac
Copy link

pacmac commented Jun 13, 2017

Thanks so much, simple when you know how :-)

I learnt something today.

@17o2
Copy link

17o2 commented Jul 17, 2017

Will this allow to wake up on either an external interrupt (pin) or a timeout (RTC) if no pin triggers it within a certain timeframe? Because that would be awesome. Can't wait!

@MrSurly
Copy link
Contributor Author

MrSurly commented Jul 17, 2017

Will this allow to wake up on either an external interrupt (pin) or a timeout (RTC) if no pin triggers it within a certain timeframe? Because that would be awesome. Can't wait!

@formatc1702: Yes, that should work.

@SmileyChris
Copy link

SmileyChris commented Aug 7, 2017

I've compiled this and tested, with everything working as expected.

I also merged against the latest esp32 branch and everything still worked as expected.

@formatc1702 I explicitly checked your case and it works fine - the following code will wake after 30 seconds or as soon as the pin is brought low:

p.init(p.IN, p.PULL_UP)
r.wake_on_ext0(pin = p, level = 0)
m.deepsleep(sleep_ms=30000)

@MrSurly I'm not sure what relevance the datetime code has to the deep sleep feature being implemented. Should that really be part of this PR?

@SmileyChris
Copy link

Out of interest I tried uncommenting the wake_on_touch function. If wake_on_touch(True) then machine.deepsleep() raises:
E (75307) deepsleep: Conflicting wake-up trigger: ext0

That can totally be a separate feature I feel. Let's just get the basics in!

@MrSurly
Copy link
Contributor Author

MrSurly commented Aug 7, 2017

@MrSurly I'm not sure what relevance the datetime code has to the deep sleep feature being implemented. Should that really be part of this PR?

This PR could be better named "Implement machine.RTC." In upstream μPy, datetime is part of machine.RTC. All of the "wake" functionality, internally, is tied to the RTC.

@nickzoic
Copy link
Collaborator

nickzoic commented Aug 7, 2017 via email

@MrSurly
Copy link
Contributor Author

MrSurly commented Aug 12, 2017

@nickzoic Speaking of which -- are there any more roadblocks to merging this?

@pacmac
Copy link

pacmac commented Sep 3, 2017

Are we ever going to have deepsleep on the esp32 ?, this seems to be taking a VERY long time and so many real-life projects are dependant upon this.

@nickzoic
Copy link
Collaborator

@dpgeorge if you're generally happy w/ this approach I'm happy to do some testing ...

@MrSurly
Copy link
Contributor Author

MrSurly commented Sep 12, 2017

I'll fix up the conflicts.

@dpgeorge
Copy link
Member

@MrSurly did you consider changing the API for pin-wakeup as proposed here: #32 (comment) ?

@MrSurly
Copy link
Contributor Author

MrSurly commented Sep 12, 2017

@dpgeorge I did definitely consider it, and I put a lot of weight on matching existing API. However, that API is very closely tied to the PyBoard hardware, and I personally don't think it's a good idea trying to make substantially different hardware adhere to it.

To recap:

  • ext0 wake is a single-pin wake-on-level, which isn't much different from how the PyBoard works.
  • ext1 allows you to define a set of multiple pins, which can be configured as either wake on "all low" or "any high"

It's ext1 that is problematic. If we do the per-pin configuration, what's the behavior for conflicting wake-up types?

I'm totally OK with changing the API, but I need a concrete example of how it could work with the ESP32 capabilities in a way that isn't unnecessarily confusing.

@dpgeorge
Copy link
Member

However, that API is very closely tied to the PyBoard hardware, and I personally don't think it's a good idea trying to make substantially different hardware adhere to it.

That API was also designed with the cc3200 in mind and the cc3200 port does implement it (for the most part). It was hoped that that API would be good enough for other MCU's as well.

It's ext1 that is problematic. If we do the per-pin configuration, what's the behavior for conflicting wake-up types?

Option 1: use the Pin.irq() method to configure everything and make it smart so it selects ext0 and/or ext1, and raises an exception if the combination of wake pins isn't supported. Eg:

pin1.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP) # configures ext0
pin2.irq(trigger=Pin.IRQ_HIGH_LEVEL, wake=machine.DEEPSLEEP) # configures ext1 with "any high"
pin3.irq(trigger=Pin.IRQ_HIGH_LEVEL, wake=machine.DEEPSLEEP) # reconfigures ext1 with "any high" with pin2 and pin3
pin4.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP) # raises an exception, no resources left

Note that pin.irq(wake=None) will disable that pin from waking.

Option 2: use the Pin.irq() method just to configure 1 pin using ext0, then a special esp method for ext1. Eg:

pin1.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP) # configures ext0
pin2.irq(trigger=Pin.IRQ_HIGH_LEVEL, wake=machine.DEEPSLEEP) # raises an exception, no resources left
import esp
esp.wake_on_ext1(pins=(...), level=...) # configure special ext1

Option 2 is simpler to implement and at least gives a similar API for the simplest case of 1 pin waking the device. But Option 1 is not that much more difficult to implement and gives more generality.

@MrSurly
Copy link
Contributor Author

MrSurly commented Sep 13, 2017

Maybe a hybrid approach. I place value on code readability, thus I think EXT1 should be explicit. I get that the code should work the same as other ports (though arguably this is definitely hardware specific), at least for EXT0, so perhaps:

pin1.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP) # configures ext0, matches CC3200
pin2.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP) # Raises exception, no resources left

pin1.irq(wake=None) # removed Pin 1 from wake
pin2.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP) # Now pin 2 works for EXT0

pin5.irq(trigger=Pin.EXT1_ANY_HIGH, wake=machine.DEEPSLEEP) # configures ext1 with "any high"
pin6.irq(trigger=Pin.EXT1_ANY_HIGH, wake=machine.DEEPSLEEP) # reconfigures ext1 with "any high" with pin2 and pin3
pin7.irq(trigger=Pin.EXT1_ALL_LOW, wake=machine.DEEPSLEEP) # raises an exception, no resources left

One other thing to consider is the wake = machine.DEEPSLEEP idiom, which kind of breaks down now that we have "light sleep".

Presently:

pin1.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP)
machine.deepsleep()```

But what does the following do?

```python
pin1.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.DEEPSLEEP)
machine.lightsleep()

I'd argue to deprecate machine.deepsleep(), and creating machine.SLEEP and machine.sleep(). That way the sleep type is triggered by something like this:

pin1.irq(trigger=Pin.IRQ_LOW_LEVEL, wake=machine.SLEEP) # << Note 'SLEEP' instead of 'DEEPSLEEP'

machine.deepsleep() # deprecated, but still exists as an alias for machine.sleep(type = machine.DEEPSLEEP)
# or
machine.sleep(type = machine.DEEPSLEEP)
# or
machine.sleep(type = machine.LIGHTSLEEP)

Using machine.DEEPSLEEP or machine.LIGHTSLEEP for the wake parameter of Pin.irq is OK, they'd be considered synonyms for machine.SLEEP.

@dpgeorge
Copy link
Member

Maybe a hybrid approach. I place value on code readability, thus I think EXT1 should be explicit.

I get your point, but the idea of machine is to be generic and provide a unified interface for all ports. Ports should try their best to conform to the API, rather than adding lots of specific things. The idea of "EXT1" is very specific to esp32 so I think if you want to make it clear that your using this special feature then it should go in the esp module (perhaps even an esp32 module since esp8266 doesn't have this feature...).

One other thing to consider is the wake = machine.DEEPSLEEP idiom, which kind of breaks down now that we have "light sleep".

The machine module has sleep() and deepsleep(). The former is used exactly in the case where execution resums from where it left off. In fact, I'd argue that machine.sleep() should be renamed to machine.lightsleep() to make this behaviour a bit more clear, and also to disambiguate from time.sleep() which has different semantics (it's not a low-power sleep).

There are also machine.SLEEP and machine.DEEPSLEEP constants to say that you want the entity (eg pin) to wake the device from these sleep states. So I think the existing API does cover the esp32's deepsleep and ligthsleep modes.

@MrSurly
Copy link
Contributor Author

MrSurly commented Sep 15, 2017

I get your point, but the idea of machine is to be generic and provide a unified interface for all ports. Ports should try their best to conform to the API, rather than adding lots of specific things. The idea of "EXT1" is very specific to esp32 so I think if you want to make it clear that your using this special feature then it should go in the esp module (perhaps even an esp32 module since esp8266 doesn't have this feature...).

Okay, then, how about:

  • Make ext0 will work as closely as possible to the current Pin.irq idiom for other ports, with the caveat that only 1 pin can be used -- configuring a second pin without deconfiguring the first means a "no resources" exception.
  • Move the ext1 and wake-on-touch to a new top-level esp32 module
  • Keep the rtc module, if only for datetime?

Issues:

  • Wake and IRQ are different concepts on the ESP32
    • Wake only supports levels, and not edges -- what to do when wake parameter is set, and the trigger is edge-based? Exception?
    • Other?

The machine module has sleep() and deepsleep(). The former is used exactly in the case where execution resums from where it left off. In fact, I'd argue that machine.sleep() should be renamed to machine.lightsleep() to make this behaviour a bit more clear, and also to disambiguate from time.sleep() which has different semantics (it's not a low-power sleep).

There are also machine.SLEEP and machine.DEEPSLEEP constants to say that you want the entity (eg pin) to wake the device from these sleep states. So I think the existing API does cover the esp32's deepsleep and ligthsleep modes.

Ah, okay. Showing my ignorance =P That will work well, then.

@pacmac
Copy link

pacmac commented Sep 27, 2017

Hello all, has this stalled again ??

@MrSurly
Copy link
Contributor Author

MrSurly commented Sep 27, 2017

Hello all, has this stalled again ??

Welcome to open source development, where everyone is busy, and nobody gets paid.

(Apologies to Whose Line Is It Anyway?)

In all seriousness, it is open source -- there's nothing stopping you from using the code right now.

Others (e.g. the SHA2017 folks) simply make their own fork and cherry pick the PRs they want. Presumably with the hope of reconciling their Python code with whatever final API shakes out in the future.

@MrSurly MrSurly closed this Sep 28, 2017
@MrSurly
Copy link
Contributor Author

MrSurly commented Sep 28, 2017

Screwed up a rebase pretty badly, reopening

@MrSurly MrSurly reopened this Sep 28, 2017
@dpgeorge
Copy link
Member

dpgeorge commented Oct 5, 2017

@MrSurly your suggestions above in #85 (comment) sound good!

Wake only supports levels, and not edges -- what to do when wake parameter is set, and the trigger is edge-based? Exception?

Yes, it should just accept the level-based trigger in the pin.irq() method.

@MrSurly
Copy link
Contributor Author

MrSurly commented Oct 5, 2017

@dpgeorge
Ok, I'll move forward when time permits.

@MrSurly
Copy link
Contributor Author

MrSurly commented Nov 5, 2017

@dpgeorge
Revisiting this. The Pin.irq() documentation states:

trigger configures the event which can generate an interrupt. Possible values are:
Pin.IRQ_FALLING interrupt on falling edge.
Pin.IRQ_RISING interrupt on rising edge.
Pin.IRQ_LOW_LEVEL interrupt on low level.
Pin.IRQ_HIGH_LEVEL interrupt on high level.
These values can be OR’ed together to trigger on multiple events.

The IDF only allows falling, rising, any edge, low, or high, which makes sense. The documentation above seems a bit odd -- why would you combine an edge trigger with a level, or why would you combine both levels?

I'm going to move forward with implementing what the IDF can actually do.

@MrSurly
Copy link
Contributor Author

MrSurly commented Nov 5, 2017

Here's the plan I'm trying to pursue for ext0:

  • Modify Pin.irq() to accept IRQ_LOW_LEVEL and IRQ_HIGH_LEVEL for use with normal IRQ handlers. It presently doesn't. officially support this.
  • Have IRQ_LOW_LEVEL and IRQ_HIGH_LEVEL also be usable by the ext0 deep sleep mode.

Problems:

  • Level-based IRQs seem to freeze the cpu when the level is asserted -- presumably because the ISR is being continuously called?
  • You can have multiple pins with level-based IRQ, but only one pin for level-based wake. Even if level-based IRQ was working, how would level-based wake work? Would it raise 'no resources' when you call machine.deepsleep(), assuming you had configured more than one pin?

Possible solution:

  • rename IRQ_LOW_LEVEL and IRQ_HIGH_LEVEL to WAKE_LOW_LEVEL and WAKE_HIGH_LEVEL, and these would only be used for wake.

@MrSurly
Copy link
Contributor Author

MrSurly commented Nov 7, 2017

Current API:

Basic sleep

import machine
machine.deepsleep() # Sleep until hard reboot
machine.deepsleep(5000) # Deep sleep for 5 seconds

machine.sleep() is implemented, but currently throws an error. If/when the IDF "light sleep" works, this can be removed.

EXT0 wakeups, using the common Micropython idiom

EXT0 wakes are for a single pin, either high or low level.

This is separate from the IRQ handler; the WAKE_LOW and WAKE_HIGH only setup the EXT0 type wake. Using a value other than WAKE_LOW or WAKE_HIGH will clear the EXT0 wake from that pin.

Additionally, any EXT0 wake will fail if esp32.wake_from_touch(True) has been called, since wake from touch and EXT0 are mutually exclusive in the IDF.

>>> import machine
>>> def h(*args): print(args)
...
>>> p1 = machine.Pin(26)
>>> p2 = machine.Pin(27)
>>> p1.irq(trigger = machine.Pin.WAKE_LOW, wake = machine.DEEPSLEEP)
<IRQ>
>>> p2.irq(trigger = machine.Pin.WAKE_LOW, wake = machine.DEEPSLEEP) 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: no resources
>>> p1.irq(trigger=machine.Pin.IRQ_RISING, handler = h) # Clears EXT0 wake
>>> p2.irq(trigger = machine.Pin.WAKE_LOW, wake = machine.DEEPSLEEP) # EXT0 now assigned to p2
<IRQ>

Other changes to machine

machine.reset_cause() has been implemented, though usually returns machine.WDT_RESET since most of the time the IDF doesn't reset properly. Does properly return machine.DEEPSLEEP_RESET when waking from deep sleep.

machine.wake_reason() has been implemented.

The new top-level esp32 module

esp32.wake_on_touch(<True|False>) Configures waking on touch. Throws ValueError('no resources') in the event that EXT0 is already configured, since they are not compatible. For this to work, you need to use machine.TouchPad().config() to configure the threshold; an interrupt handler is not required.

esp32.wake_on_ext0(<Pin>, level = <0 | 1>). Configures the EXT0 wake. This will raise ValueError('no resources') if esp32.wake_on_touch(True) has been called. It will not raise this error if another pin has already been configured, it will simply configure the new pin for EXT0 and deconfigure the old pin. If level is not specified, then it is unchanged, and defaults to 0 (low).

esp32.wake_on_ext1(pins = <sequence of Pin object>, level = <esp32.WAKEUP_ALL_LOW | esp32.WAKEUP_ANY_HIGH> ): As described above, wake on multiple pins, either "all low" or "any high".

The new machine.RTC module

machine.RTC.datetime() and machine.RTC.init() work as documented in mainline MicroPython.

machine.RTC.memory() Without arguments returns the RTC nonvolatile memory, as bytes. To write memory, pass a bytes, bytearray, or str object.

@MrSurly
Copy link
Contributor Author

MrSurly commented Nov 7, 2017

@pacmac @nickzoic @SmileyChris

Any testing/comments you could provide would be appreciated.

@MrSurly
Copy link
Contributor Author

MrSurly commented Nov 7, 2017

Oh, also, shout out to @manningt for the RTC.memory() implementation.

@MrSurly
Copy link
Contributor Author

MrSurly commented Nov 7, 2017

Squashed, pushed.

* Implement deep sleep using the Pin.irq() idiom
* Stub for light sleep, when it works in the IDF
* Implement machine.reset_cause()
* Implement machine.wake_reason()
* Add top-level esp32 module
  * esp32.wake_on_touch()
  * esp32.wake_on_ext0()
  * esp32.wake_on_ext1()
* Implement machine.RTC
  * machine.RTC.datetime()
  * machine.RTC.memory()
@MrSurly
Copy link
Contributor Author

MrSurly commented Nov 10, 2017

NUDGE

Ready for testing/merging

@nickzoic
Copy link
Collaborator

nickzoic commented Nov 10, 2017 via email

@mactijn
Copy link

mactijn commented Dec 1, 2017

I've been running a prototype alarm clock with this branch for a few days now on an MH-ET Live Mini. Works completely as expected using the ntptime module from esp8266. Thanks for your work!

EDIT: if there's anything specific you'd like to have feedback on, please say so, I'd be happy to test things out.

@brandond
Copy link

brandond commented Dec 8, 2017

Just started hacking on ESP32s this week. Would love to see this land ASAP!

@carstenblt
Copy link

Nice work, RTC() works as expected for me.

utime however counts from 2000, whereas RTC() counts from 1970 - utime.localtime() is off by 30 years.

@manningt
Copy link

Counting from year 2000 is intended - refer to: https://docs.micropython.org/en/latest/esp8266/library/utime.html?highlight=utime

@MrSurly
Copy link
Contributor Author

MrSurly commented Dec 27, 2017

utime however counts from 2000, whereas RTC() counts from 1970 - utime.localtime() is off by 30 years.

utime is correct. Just read the docs for RTC(), and it doesn't specify if RTC.init() is supposed to be 1970 or 2000. I think it needs to be 2000.

@MrSurly
Copy link
Contributor Author

MrSurly commented Dec 31, 2017

Moved to the main MP repo

@MrSurly MrSurly closed this Dec 31, 2017
@choies1
Copy link

choies1 commented Feb 16, 2018

I downloaded "esp32-20180216-v1.9.3-315-g73d1d20b.bin (latest)" in 'http://www.micropython.org/download#esp32' to my ESP32 board.

However, I can't use RTC. What should I do to use RTC?

Followings are error messages.

>>> from machine import RTC
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: cannot import name RTC

@dpgeorge
Copy link
Member

@choies1 this feature (machine.RTC) was only just merged. It should appear in the next latest build, shortly.

@choies1
Copy link

choies1 commented Feb 17, 2018

RTC works from 'esp32-20180217-v1.9.3-318-g60c6b880.bin (latest)'.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.