-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Alarms, light sleep, and deep sleep #2796
Comments
Below is an example. It is the deep sleep variant of this light sleep example. import alarm.pin
import alarm.time
import sleepio
alarm = sleepio.wake_alarm
pin0_alarm = alarm.pin.PinLevelAlarm(board.IO0, True, enable_pull=True)
time_alarm = alarm.time.TimeAlarm(time.monotonic() + 10.0)
# do something based on what woke us up
if alarm == pin0_alarm:
print("pin change")
elif alarm == time_alarm:
print("time out")
sleepio.set_reload_alarms(pin0_alarm, time_alarm)
# Finish code.py which will start the alarms. When on USB it will lead to a light sleep
# to preserve the USB connection and reload when an alarm goes off. When USB is not
# connected, it will deep sleep and restart CircuitPython on wakeup. Updated for newer sleepio API. |
@tannewt and I discussed this a few days ago, and I've been working on a further refinement of the API. I will use the terminology "fired" to indicate an alarm "triggering" or "going off". "Going off" is a bit confusing, since it could sound like "turning off". I would like to avoid talking about "deep sleep" and "light sleep" in the API, since there are circumstances when the implementation might not actually want to sleep, such as when connected to USB and enumerated. Instead, there are different circumstances in which an alarm can fire. CPy can take advantage of waiting for an alarm and do a light or deep sleep, but it doesn't have to. I have added an alarm queue mechanism, instead of having I have also made the alarm objects more autonomous. In @tannewt's proposal above, the program enables the alarms by informing An alarm is not automatically enabled when it is instantiated. Instead, it is explicitly enabled. Once an alarm fires, it becomes disabled, until it is explicitly re-enabled again. An alarm can fire during normal program operation, during a program-initiated sleep, or after a program has shut down. When an alarm fires, it is recorded in an event queue. For now is just a list, since we don't have a native queue type. The default event queue is Here are the detailed cases when an alarm can fire:
import supervisor
import alarm.pin
import alarm.time
# Did we restart because there was an alarm?
# supervisor.runtime.alarms is a list of alarms that have fired. It is the default event queue.
alarm = supervisor.runtime.alarms.get(0, None)
# Create some alarms. They are not yet enabled.
# trigger can be HIGH, LOW, RISING, FALLING (or RISE and FALL ?)
pin5_alarm = alarm.pin.PinAlarm(board.IO5, trigger=alarm.pin.HIGH, pull=digitalio.Pull.UP)
pin6_alarm = alarm.pin.PinAlarm(board.IO6, trigger=alarm.pin.RISING, pull=digitalio.Pull.UP)
# Arg to TimeAlarm is the number of seconds after the alarm is enabled (not the time at instantiation),
# or it's a struct time, designating a date/time in the future.
time_alarm = alarm.time.TimeAlarm(time.monotonic() + 60)
# Check to see whether an alarm caused us to wake up from shutdown
if alarm == pin5_alarm:
print("pin5 change")
elif alarm == time_alarm:
print("time out")
# Enable a pin alarm after the program finishes. This is the deep sleep case.
# Not enabled during normal program flow. Will enqueue on the default queue.
# Passing False disables the alarm if it is enabled.
pin5_alarm.enable_after_shutdown(True)
# Enable alarm now. It will trigger during normal operation, during time.sleep(), or during
# supervisor.runtime.wait_for_alarm(). Enqueue on the default queue when fired.
time_alarm.enable_now(True)
# Enable alarm now, but enqueue on an alternate queue when fired.
my_queue = []
pin6_alarm.enable_now(True, queue=my_queue)
# Wait for any .enable_now alarm.
supervisor.runtime.wait_for_alarm()
# Fetch the alarm that fired.
alarm = supervisor.runtime.alarms.get(0, None)
my_alarm = my_queue.get(0, None)
# Any .enable_now alarm can fire during this time.
time.sleep(5)
|
What is the difference between a pin level alarm and a pin edge alarm? Won't a low level alarm fire at the same time as falling edge would? My biggest concern is having each alarm enabled independently of each other and independent of use. This prevents validating that 1) the alarms can all be set at the same time and 2) they can all work at the desired sleep level. 1 is important because pin alarming might not actually be independent. On the ESP32-S2 (reference) only one pin can be checked independently with ext0. ext1 can check multiple pins but they are either an NAND 2 is important because some alarms may only work in higher sleep states. For example, we can check pin levels independently on the S2 if the GPIO peripheral is active and can trigger interrupts. I prefer preserving My proposal for listening for a set of alarms while running code would be to add a call similar to However, I'm not certain this is what we'd need for asyncio integration. I'd much rather focus on light and deep sleep only. |
fyi some chips have irq-on-level and some have irq-on-edge - i suppose it only really matters if the irq pin is LOW on entry-to-sleep, if its falling-edge it would not wake immediately. If its level it would exit sleep immediately. |
I have lost track of the changes in sleepio and it seems like the api has become quite complex. |
@tannewt and I talked again today, and revised the example in @tannewt's comment above. A few more notes: Re rising/falling edge detection: Both level and transition can often be detected. I have seen both, and I even see "transition either direction". Waking from deep sleep on pin change: On a number of chips, this is implemented as "tamper detection". On nRF, it is just called "DETECT") Some chips just do level detection (SAMD51, nRF). SAMD51 only does it on a few pins (5). STM does only rising/falling, it appears (EXTI registers). nRF has an "or" kind of function (LDETECT), vaguely like the ESP32, maybe. On light sleep, most chips can do level or transition. MicroPython does interrupt handlers for pins like this:
|
I am now working on this and revising the API toward the revised example above. I am making some changes, and I have some suggested changes that I would welcome comments on:
|
Thanks for picking this work up! I'm excited to try it.
I worry that just
I don't buy this argument. Each additional entry in the module table is only 8 bytes. There are only a few existing APIs for the module so it'd only be ~40 bytes or so. That isn't worth an inconsistent API.
I'd prefer it in the new module so that we don't need to implement it for all platforms and can convey when it is implemented. Importing a native module to get access to it is low cost.
I have no problem with the nested modules because they can fail on import. I don't like the higher level classes though because it doesn't fail on import and isn't obvious to convey when something is supported. Having multiple top level modules is the simplest and most consistent option. |
Alarms sounds a lot like interrupt handlers. Even if they don't use interrupts I think it would be a bad idea if they are exclusive to the |
That was my proposal in this comment: #2796 (comment). I agree we could add this later, in some way, and that's a motivation for creating a separate top-level |
@tannewt I had one more idea this morning, which is not to move Otherwise what I see is that These are all cosmetic and do not affect me proceeding on the implementation. |
@dhalbert merging |
That sounds nice to me too :) |
Looking from the point of view of implementing a main loop for async, would there be a way to wait (sleep until) on several different conditions at once? For example, a timeout, a pin raise, a socket connection, a user input on STDIO and a UART transmission? Because that's what we would ultimately need. |
@deshipu Yes, multiple alarms/conditions can be enabled at once. Also, there is provision to get the alarm that caused wake-up + parameters associated with it. |
What I'm worried about the most is that there won't be alarms available for some of the conditions we want to wait on. But I suppose we can just keep adding new kinds of alarms? |
Alarms are implemented as separate modules and there availability will vary on a port specific basis. |
Another thing is that we want to be able to wait on an explicit list of alarms, not necessarily on all alarms that are enabled. That's because there may be user alarms created and enabled independent from the async main loop, that we would need to ignore somehow. |
There will be For deep sleep, The names above are tentative; I'm still thinking about the best naming. For an async loop, I proposed above that alarms that trigger will put themselves on a queue, but the API and semantics are not yet specified. You don't have to be asleep for that to happen. We can open a new issue for that. It will not be in the first version of this: our short-term highest priority is deep sleep on the ESP32-S2, for battery reasons. |
Ligh sleep example, copied from #2795. Exact function names may change, and Here is an example: import sleepio
import time
import alarm.pin
import alarm.time
pin0_alarm = alarm.pin.PinLevelAlarm(board.IO0, True, enable_pull=True)
time_alarm = alarm.pin.TimeAlarm(time.monotonic() + 10.0)
while True:
# start with a light sleep until an alarm triggers.
alarm = sleepio.sleep_until_alarm(pin0_alarm, time_alarm)
# do something based on what alarmed
if alarm == pin0_alarm:
print("pin change")
elif alarm == time_alarm:
print("time out") [Example updated for newer sleep API, see #2796] |
@dhalbert that looks good, I suppose I got confused by the triggered alarms being stored on a single queue. To implement this, we need to be able to remove the alarms we are waiting on from the queue, but at the same time leave all the other alarms in there. That doesn't sound like a queue data structure anymore. Unless you want to just put the events that don't match our list back on the queue? What happens when there are two different alarms for the same thing? Will that create duplicate events? |
@deshipu In the sleep case, there is no queue. I was making a strawman proposal for non-sleep event queuing. I think it needs a lot more thought, but could be added to |
@dhalbert I believe that API pattern is suitable for integration with the tasko event loop. Looking forward to playing with alarms, thanks for working on this! |
This is exactly why I don't want us to think about async now. When we do async I think we'll want to look at all of our existing APIs and provide async friendly versions. This is a huge task and a distraction from adding sleep support that is broadly useful. |
@tannewt Fair enough. I think that leaves two use cases, I suppose: waiting on an interrupt pin of a sensor, and waking up periodically to make measurements? Maybe also a soft power button? |
Yup, I think we get pretty far with just pin level alarm and time based alarm. |
Is there a branch for testing this out? This could be super handy for my magtag |
This is my first priority right now. Iwill submit a PR as soon as something is working. The ESP32-S2 is the first targeted port. |
Partly fixed by #3467. Let's close this and open more task-specific issues. |
After we have #2795 we'll have the mechanic for waking on a variety of sources.
We should add the ability to set what alarms should cause a reload when the user code is finished. This function should validate that the alarms can be run at the lowest power setting.
All RAM and program state can be lost in this sleep state. So, we also need a way to read the wake up cause from user code. CircuitPython should also start up faster when woken by an alarm by skipping things like the safe mode reset delay.
This should work the same when on USB except that the sleep will be WFI only so that USB still works. Any USB writes will reload and clear alarms. Otherwise the alarms will cause a reload with the appropriate state.
The text was updated successfully, but these errors were encountered: