Terminology

We'll distinguish between deep sleep and light sleep:

  • If a program does a deep sleep, it first exits, and then the microcontroller goes to sleep, turning off as much as possible while still being able to wake up later. When the microcontroller wakes up, it will start your program (code.py) from the beginning.
  • If a program does a light sleep, it still goes to sleep but continues running the program, resuming after the statement that did the light sleep. Power consumption will be minimized. However, on some boards, such as the ESP32-S2, light sleep does not save power compared with just using time.sleep().

CircuitPython uses alarms to wake up from sleeping. An alarm can be triggered based on a specified time being reached, or based on an external event, such as a pin changing state. The pin might be attached to a button, so you would be able to wake up on a button press.

The alarm module

Alarms and sleep are available in the alarm module in CircuitPython (latest documentation). You create one or more alarms, and then go into a light sleep or deep sleep while waiting for them.

TimeAlarm Light Sleep

Here's a simple program that just blinks the status NeoPixel every 10 seconds, and does a light sleep in between, using a TimeAlarm. The video above demonstrates this program, eliding the 10-second sleeps.

import alarm
import board
import digitalio
import neopixel
import time

# On MagTag, enable power to NeoPixels.
# Remove these two lines on boards without board.NEOPIXEL_POWER.
np_power = digitalio.DigitalInOut(board.NEOPIXEL_POWER)
np_power.switch_to_output(value=False)

np = neopixel.NeoPixel(board.NEOPIXEL, 1)

while True:
    np[0] = (50, 50, 50)
    time.sleep(1)
    np[0] = (0, 0, 0)

    # Create a an alarm that will trigger 10 seconds from now.
    time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 10)

    # Do a light sleep until the alarm wakes us.
    alarm.light_sleep_until_alarms(time_alarm)
    # Finished sleeping. Continue from here.

TimeAlarm Deep Sleep

Here's a similar program, which does a deep sleep. The video above is still what you'd see. Remember that for deep sleep, the program exits, and restarts when woken up. So in this program there's no while True: loop.

import alarm
import board
import digitalio
import neopixel
import time

# On MagTag, enable power to NeoPixels.
# Remove these two lines on boards without board.NEOPIXEL_POWER.
np_power = digitalio.DigitalInOut(board.NEOPIXEL_POWER)
np_power.switch_to_output(value=False)

np = neopixel.NeoPixel(board.NEOPIXEL, 1)

np[0] = (50, 50, 50)
time.sleep(1)
np[0] = (0, 0, 0)

# Create a an alarm that will trigger 20 seconds from now.
time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 20)
# Exit the program, and then deep sleep until the alarm wakes us.
alarm.exit_and_deep_sleep_until_alarms(time_alarm)
# Does not return, so we never get here.

PinAlarm Deep Sleep

This example uses PinAlarm instead of TimeAlarm. It will deep sleep until the D11 button on the lower right of the MagTag is pressed. On the MagTag, pressing a button connects a pin to ground, so we wait for a False value. We also enable a pull-up to hold the pin high (True) when the button is not pressed.

import alarm
import board
import digitalio
import neopixel
import time

# On MagTag, enable power to NeoPixels.
# Remove these two lines on boards without board.NEOPIXEL_POWER.
np_power = digitalio.DigitalInOut(board.NEOPIXEL_POWER)
np_power.switch_to_output(value=False)

np = neopixel.NeoPixel(board.NEOPIXEL, 1)

np[0] = (50, 50, 50)
time.sleep(1)
np[0] = (0, 0, 0)

pin_alarm = alarm.pin.PinAlarm(pin=board.D11, value=False, pull=True)

# Exit the program, and then deep sleep until the alarm wakes us.
alarm.exit_and_deep_sleep_until_alarms(pin_alarm)

# Does not return, so we never get here.

TouchAlarm Deep Sleep

This example is for the Metro ESP32-S2. The MagTag has no pins that can be used for touch. (D10 could theoretically be used, but protection components are connected to it that prevent it being used for touch.)

It will sleep until pin IO5 is touched, or 10 seconds has elapsed, whichever comes first. The on-board LED blinks for one second at the beginning of the program.

import alarm
import board
import digitalio
import time

# Print out which alarm woke us up, if any.
print(alarm.wake_alarm)

led = digitalio.DigitalInOut(board.LED)
led.switch_to_output(value=True)
time.sleep(1)
led.value = False

# Create an alarm that will trigger 10 seconds from now.
time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + 10)
# Create an alarm that will trigger if pin IO5 is touched.
touch_alarm = alarm.touch.TouchAlarm(pin=board.IO5)

# Exit the program, and then deep sleep until one of the alarms wakes us.
alarm.exit_and_deep_sleep_until_alarms(time_alarm, touch_alarm)
# Does not return, so we never get here.

Pretending to Sleep When Connected

When your board is connected to a host computer via USB, you don't want it to really do a light sleep or deep sleep, because that would break the USB connection and make it difficult to debug or edit your program. So when the board is connected, we simulate light and deep sleep. If CircuitPython can reduce power consumption while pretending to sleep and still remain connected, it will do so, but you will not be saving nearly as much power as when you're not connected.

So if you're trying to measure how much power you're saving while sleeping, you really need to do your measurements from battery power (or a power supply) while unconnected.

What Woke Me Up?

When a program awakens from light sleep or deep sleep, it's because of an alarm. You can find out what kind of alarm woke up the program by looking at alarm.wake_alarm.

If the program did not wake up from sleep, then alarm.wake_alarm will be None

If the program woke up from a light sleep, then alarm.wake_alarm will be one of the alarm objects passed to alarm.light_sleep_until_alarms(...). The triggered alarm is also returned by that function, so you can get the alarm value directly. Here are two ways to get the triggered alarm:

triggered_alarm = alarm.light_sleep_until_alarms(alarm1, alarm2)

# is the same as

alarm.light_sleep_until_alarms(alarm1, alarm2)
triggered_alarm = alarm.wake_alarm

If the program restarted after a deep sleep, then alarm.wake_alarm will be an alarm object of the same type as the original alarm, but it will not be exactly the same object. It's attributes may be different, or they may be incomplete in some way. For instance, for TimeAlarm, the .monotonic_time attribute may not contain the same value. But you can still do an isinstance(alarm.wake_alarm, TimeAlarm) to find out it was a TimeAlarm that woke up the program.

This guide was first published on Dec 17, 2020. It was last updated on Mar 08, 2024.

This page (Alarms and Sleep) was last updated on Mar 08, 2024.

Text editor powered by tinymce.