Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Powman – Power Management Library for Micropython

The main goal is to reproduce similar low-power timer and wake-up functionalities in Micropython. It provides direct access to hardware registers through memory-mapped I/O and is intended for bare-metal embedded development.

This project is inspired by and based on the powman library implementation available in the Raspberry Pi Pico SDK

The library directly accesses the RP2350 power management and timer registers via memory-mapped I/O. Official documentation can be found in the RP2350 datasheet, especially in Section 6.0 (Power Management Overview and Section 6.4 (Register Configuration)

It is designed to control low-power modes, timers, and wake-up alarms on Raspberry Pi Pico 2.

Here for tinygo library

Requirements

  • Micropython
  • Raspberry Pi Pico 2

Usage

Important: powmanGetWakeupReason() must be called before powmanInit(), because init writes to POWMAN registers that may affect wake-up state.

1. Check wake-up reason

import deepsleep

reason = deepsleep.powmanGetWakeupReason()

if reason == 0:
    print("fresh boot")
elif reason & deepsleep.WAKEUP_ALARM:
    print("woke from timer")
elif reason & deepsleep.WAKEUP_GPIO0:
    print("woke from GPIO")

2. Initialize the timer

Set the absolute system time in milliseconds (must be > 0):

deepsleep.powmanInit(1704067200)

3. Enter deep sleep

Sleep for a fixed duration (milliseconds):

deepsleep.powmanOffForMs(10000)  # sleep 10 seconds

Or sleep until a GPIO goes HIGH:

deepsleep.powmanOffUntilGPIO(15)  # wake on GP15 HIGH

Both functions never return — the chip reboots on wake-up.


Full example

import deepsleep
import time

def main():
    reason = deepsleep.powmanGetWakeupReason()  # before init!

    if reason == 0:
        print("fresh boot")
    elif reason & deepsleep.WAKEUP_ALARM:
        print("woke from timer alarm")
    elif reason & (deepsleep.WAKEUP_GPIO0 | deepsleep.WAKEUP_GPIO1 |
                   deepsleep.WAKEUP_GPIO2 | deepsleep.WAKEUP_GPIO3):
        print("woke from GPIO")

    deepsleep.powmanInit(1704067200)
    deepsleep.powmanOffForMs(5000)

main()

In main.py there is a minimal example`.

Power consumption over one minute.

Results

The consumption during low power mode

How It Works

Powman directly accesses memory-mapped registers to control:

  • System timer
  • Alarm registers
  • Power regulator
  • Boot configuration
  • Interrupt enable flags

The library uses mem32 internally to read and write hardware registers.

This is required for bare-metal development and is safe in this context.


API Reference

Note: every powmanOff*() function below automatically forces GP0-22 to LOW before sleeping — actively driven (machine.Pin(gpio, Pin.OUT, value=0)), not just a weak pull — except whichever GPIO(s) you passed in as wake-up pins, or listed in the optional excludeGpios=(...) argument every powmanOff*() accepts. GPIO pads live in the always-on domain, so they stay live throughout dormant sleep regardless of what else is powered down; this doubles as a way to cut power to an accessory wired to a GPIO instead of 3V3 (which can't be switched off from software — see the capacitive-touch-button example in main.py). GP23-29 are left untouched (reserved for the wireless chip / ADC diode caveat on Pico 2 W — see the lowPowerWifiChip flag on powmanInit below).

Careful: this actively drives every unexcluded GP0-22 — if any of them is wired to something else that's also actively driving it (not just a passive load), the two will fight. Pass every such GPIO in excludeGpios to leave it alone.

powmanGetWakeupReason() → int

Returns the reason for the last wake-up as a bitmask. Must be called before powmanInit().

Constant Value Meaning
0 0x00 Fresh boot / software reset
WAKEUP_CHIP_RESET 0x01 Chip-level reset
WAKEUP_GPIO0 0x02 Wake from PWRUP0 (used by powmanOffUntilGPIO)
WAKEUP_GPIO1 0x04 Wake from PWRUP1
WAKEUP_GPIO2 0x08 Wake from PWRUP2
WAKEUP_GPIO3 0x10 Wake from PWRUP3
WAKEUP_ALARM 0x40 Wake from timer alarm

The register is a bitmask: multiple bits can be set simultaneously. Use & to test individual sources.

Internally reads CHIP_RESET.HAD_SWCORE_PD (bit 25) to confirm a POWMAN sleep occurred, then reads LAST_SWCORE_PWRUP (offset 0xA0) for the source.


powmanInit(absTimeMs: int, lowPowerXosc=False, lowPowerRosc=False, lowPowerPlls=False, lowPowerUsbPhy=False, lowPowerWifiChip=False, excludeGpios=())

Initializes the POWMAN timer with an absolute timestamp in milliseconds (must be > 0).

The lowPower* flags enable extra, optional current-saving steps on top of the normal SWCORE/XIP/SRAM power-down (stopXosc/stopRosc/powerDownPlls/isolateUsbPhy/powerDownWifiChip, all implemented in deepsleep.py). Enabling them here means every subsequent powmanOff*() call applies them automatically — no need to build a beforeSleep callback yourself. lowPowerXosc takes effect immediately; the other four are deferred and applied last, right before the chip halts.

Flag Effect
lowPowerXosc Stops the external crystal oscillator.
lowPowerRosc Stops the ring oscillator too (implies clocks drop to POWMAN's 32.768 kHz LPOSC for whatever code runs after this point — arm everything before enabling this).
lowPowerPlls Powers down PLL_SYS/PLL_USB. Requires lowPowerXosc (clk_sys must already be off the PLL path).
lowPowerUsbPhy Re-isolates the USB PHY (matches the RP2350 datasheet's own low-power test methodology).
lowPowerWifiChip Pico 2 W only. Powers down the CYW43439 wireless companion chip. No-op on plain Pico 2, or if the chip was never powered up.
excludeGpios GPIO(s) — a single int or a list/tuple — that the automatic GP0-22 force-low pass should always leave alone, on every subsequent powmanOff*() call. Combined with whatever excludeGpios each individual call also specifies.

Risk: lowPowerRosc and lowPowerPlls carry real risk — if clocks aren't moved off an oscillator/PLL before it's stopped, the chip hangs and needs a physical reset/reflash (BOOTSEL) to recover. Only tested on Pico 2 (RP2350) with stock boot clock configuration. lowPowerUsbPhy/lowPowerWifiChip are low risk (no clock changes).

For maximum power savings:

import deepsleep

deepsleep.powmanInit(1704067200, lowPowerXosc=True, lowPowerRosc=True,
                      lowPowerPlls=True, lowPowerUsbPhy=True, lowPowerWifiChip=True)

deepsleep.powmanOffForMsOrGPIO(10000, [(8, True)])

In testing (Pico 2 W, lowPowerXosc/lowPowerRosc/lowPowerUsbPhy/lowPowerWifiChip enabled), sleep current went from ~600µA to ~230-250µA measured on VSYS. lowPowerPlls has not been measured yet.


powmanOffForMs(sleepingMs: int, beforeSleep=None, excludeGpios=())

Enters deep sleep and reboots after sleepingMs milliseconds. Never returns. Since there's no wake-up GPIO here, every GP0-22 not listed in excludeGpios gets forced low.


powmanOffUntilGPIO(gpio: int, high: bool = True, slot: int = PWRUP0, beforeSleep=None, excludeGpios=())

Enters deep sleep and reboots when the specified GPIO pin reaches the target level. gpio must be 0–49. Never returns.

Parameter Description
gpio GPIO pin number (0–49)
high True = wake on HIGH, False = wake on LOW
slot Which of the 4 PWRUP registers to use (PWRUP0..PWRUP3). Defaults to PWRUP0.
beforeSleep Optional callable, invoked last, right before the chip actually halts.
excludeGpios Extra GPIOs (besides gpio itself) to leave untouched by the automatic force-low pass — see the note at the top of the API Reference.

Important: The GPIO must already be at the opposite level before calling this function. POWMAN requires a level transition to fire — if the GPIO is already at the wake level when sleep is entered, the chip will never wake.

high GPIO must be before sleep Wake trigger
True LOW GPIO goes HIGH
False HIGH GPIO goes LOW

powmanOffUntilAnyGPIO(pins: list[tuple[int, bool]], beforeSleep=None, excludeGpios=())

Enters deep sleep and reboots when any of up to 4 GPIO pins reaches its target level. pins is a list/tuple of up to 4 (gpio, high) pairs, each mapped to one of the 4 independent PWRUP wake-up slots. Never returns.

# wake when GP15 goes HIGH or GP16 goes LOW
deepsleep.powmanOffUntilAnyGPIO([(15, True), (16, False)])

The same transition requirement as powmanOffUntilGPIO applies to every pin. After reboot, use powmanGetWakeupReason() to tell which one fired: pins[0]WAKEUP_GPIO0, pins[1]WAKEUP_GPIO1, and so on.


powmanOffForMsOrGPIO(sleepingMs: int, pins: list[tuple[int, bool]], beforeSleep=None, excludeGpios=())

Enters deep sleep and reboots when either the timer alarm expires or any of up to 4 GPIO pins reaches its target level — whichever happens first. Never returns.

# wake after 10s, or immediately if GP15 goes HIGH before that
deepsleep.powmanOffForMsOrGPIO(10000, [(15, True)])

Same transition requirement as powmanOffUntilGPIO/powmanOffUntilAnyGPIO applies to every pin. After reboot, powmanGetWakeupReason() tells you which source actually fired: WAKEUP_ALARM for the timer, WAKEUP_GPIO0..WAKEUP_GPIO3 for pins[0]..pins[3].


Debugging tip: no serial output after wake-up

Waking up (from timer or GPIO) triggers a full chip reboot, which also resets the USB stack. Your serial terminal has to reconnect to the newly re-enumerated USB device, and usually doesn't do so fast enough to catch the first print() calls in main().

To verify wake-up behavior without relying on the serial terminal, blink the onboard LED a different number of times depending on powmanGetWakeupReason() — it's visible immediately and doesn't depend on USB reconnecting.

from machine import Pin
import time, deepsleep

def blink(times):
    led = Pin("LED", Pin.OUT)
    for _ in range(times):
        led.on()
        time.sleep_ms(200)
        led.off()
        time.sleep_ms(200)

def main():
    reason = deepsleep.powmanGetWakeupReason()  # before init!

    if reason & deepsleep.WAKEUP_GPIO0:
        blink(1)   # woke from GP8
    elif reason & deepsleep.WAKEUP_GPIO1:
        blink(2)   # woke from GP9
    elif reason & deepsleep.WAKEUP_GPIO2:
        blink(3)   # woke from GP10
    elif reason & deepsleep.WAKEUP_GPIO3:
        blink(4)   # woke from GP11
    elif reason & deepsleep.WAKEUP_ALARM:
        blink(5)   # woke from timer alarm
    else:
        blink(6)   # fresh boot / other

    deepsleep.powmanInit(1704067200)
    deepsleep.powmanOffUntilAnyGPIO([(8, True), (9, True), (10, True), (11, True)])

main()

Completed

  • Timer-based deep sleep (powmanOffForMs)
  • GPIO wake-up (powmanOffUntilGPIO)
  • Multi-GPIO wake-up (powmanOffUntilAnyGPIO)
  • Combined timer + multi-GPIO wake-up (powmanOffForMsOrGPIO)
  • Wake-up reason detection (powmanGetWakeupReason)
  • Optimize power management by disabling unnecessary components (lowPowerXosc/lowPowerRosc/lowPowerPlls/lowPowerUsbPhy/lowPowerWifiChip on powmanInit) — ~600µA → ~230-250µA measured on Pico 2 W (lowPowerPlls not yet measured)
  • Automatic force-low of unexcluded GPIOs (GP0-22) before sleep via excludeGpios, to avoid leakage from floating inputs and to let accessories be powered from a GPIO instead of 3V3 (which can't be switched off from software)

Future Developments

  • Implement a sleep mode that does not reboot the system and preserves the values of variables like machine.lightsleep()
  • Edge-triggered GPIO wake-up as an alternative to the current level-triggered mode (PWRUPx.MODE bit, never set so far)

Safety Notes

  • Only use in embedded/bare-metal environments.
  • Incorrect register values may brick your device.

Supported Platforms

Tested primarily on:

  • Raspberry Pi Pico 2

About

Micropython library for Raspberry Pi Pico 2 to put device in Deep Sleep (low power mode)

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages