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.
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:
PinAlarm Deep Sleep
This example uses PinAlarm instead of TimeAlarm. It will deep sleep until one of the D11, D12, D14, or D15 buttons on the MagTag is pressed. On MagTag boards, pressing a button connects a pin to ground, so we wait for a False value.
Setting pull=True in the PinAlarm constructor enables a pull-up or pull-down, to pull the pin to the level opposite that of value. In this case, value is set to False, so setting pull to True will enable a pull-up, to hold the pin high until pressing the button pulls it low.
(If value were set to True, setting pull=True would enable a pull-down.)
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)
# Choose a color based on which button woke us up.
COLORS = {
board.D11 : (50, 0, 0), # red
board.D12 : (0, 50, 0), # green
board.D14 : (0, 0, 50), # blue
board.D15 : (50, 50, 0), # yellow
}
wake_alarm = alarm.wake_alarm
if isinstance(wake_alarm, alarm.pin.PinAlarm):
np[0] = COLORS[wake_alarm.pin]
else:
# If not a pin wakeup, display white.
np[0] = (50, 50, 50)
time.sleep(1)
np[0] = (0, 0, 0)
# Since value=False, setting pull=True will enable a pull-up.
pin_alarm_d11 = alarm.pin.PinAlarm(pin=board.D11, value=False, pull=True)
pin_alarm_d12 = alarm.pin.PinAlarm(pin=board.D12, value=False, pull=True)
pin_alarm_d14 = alarm.pin.PinAlarm(pin=board.D14, value=False, pull=True)
pin_alarm_d15 = alarm.pin.PinAlarm(pin=board.D15, value=False, pull=True)
# Exit the program, and then deep sleep until an alarm wakes us.
alarm.exit_and_deep_sleep_until_alarms(pin_alarm_d11, pin_alarm_d12, pin_alarm_d14, pin_alarm_d15)
# Does not return, so we never get here.
Release Pins if Needed
The MagTag and other boards have helper libraries that take control of button pins. If you would like to use a PinAlarm or TouchAlarm when using such a library, you must deinitalize the pins that were taken by the library.
For the adafruit_magtag library, call magtag.peripherals.deinit() to release the pins so they can be used with an alarm.
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, CircuitPython simulates light and deep sleep. This is often called "fake light sleep" or "fake 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.
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.
Page last edited October 03, 2025
Text editor powered by tinymce.