Pi Pico - Sleep & Dormant states

PR2040 data sheet lists Sleep State & Dormant State.
Tested the implementation of these functions for the Pi Pico in Micro Python & Circuit Python using Thonny.

Both implementations work ok; the Pico sleeps for the set number of seconds in Sleep State and resets after the set number of seconds in Dormant State; as you would expect. But …
… power source connected to VSYS, USB connection removed, current meter to measure power. The results are no where near the claimed 1.3mA or 0.8mA.

Micro Python - no real change from 20mA when processor running.
Circuit Python - 15mA for Sleep, 5.5mA for Dormant.

So Circuit Python works best, maybe it does not turn off all clocks or something else.
Hard to figure what it is doing from the source code. It is nothing like the Pico-Extras example.

Now trying to wade through Ardunio C++ install for Pi Pico. Can load Blink and it works nicely.
Thonny & Python makes coding IDE for the Pico, easy, C++ is a little harder.
Visual Studio Code … forget about that one, too complex for me at this time.

So anyone else had similar experience or been able to get low power consumption for the Pico working to the levels of the datasheet.

Regards
Jim

3 Likes

Hey Jim,

Just had a quick dig. Here are the hello_sleep and hello_dormant examples, however this sleep.c seems much better documented. From my quick googling it seems there’s not much on this outside the heads of the developers at raspberry pi.

My first hunch is maybe it’s not currently possible to get a proper sleep state with a micro-python implementation? Just because there needs to be code running an interpreter for micropython, so maybe a proper sleep state has to be implemented in compiled code - which my require a pre-compiled library/binary just for that which can be called on to put the RP2040 to sleep and wake it up again to run Python?

Just spit-balling here…

3 Likes

@Oliver33 Thanks for your information and suggestions.
Eventually got Visual Studio Code running and working on a Pi 4; now understand how to use VSC.

Tried the Sleep.c example, no success.
I needed to make changes to CMakeLists.txt in the pico-extras pico_sleep library. The include statement failed to find the library because it was not referenced in the make file.

The sleep library messes with the oscillator and that is where if hangs up, it seems to run the mico much slower. Not really sure what the library is trying to do in this case, does not seem to match up with the RP2040 data sheet. IMHO there is a lot of work needed before it will be useful to most users. (as stated it is work in progress)

Conclusion: Circuit Python code will reduce the power nicely, but not as low as the datasheet.
Circuit Python comes with other problems though, it takes a lot of the Pi Pico memory and loads rather slowly. At this time the best development environment seems to be Micro Python and Thonny.

Cheers
Jim

3 Likes

SUCCESS !!!

Delved a bit deeper into how sleep.c and hello_sleep work, found where I was going wrong.
Used the on board LED to indicate the program states.

5.07V supply to VSYS
Pico startup with LED ON = 20.5mA, LED OFF = 19.1mA.
Pico sleep = 1.3mA.
Wake from sleep = 3.5mA. (some clocks not activated, USB, ADC, PLL, etc)
(see pics)

So power measured is the same as the datasheet. I can now use this in my project.
In Sleep mode the Pico starts execution from the line after it goes to sleep.
In Dormant mode the Pico essentially reboots, which is exactly what I want for my project.
Next test will be dormant mode.


Program code for putting Pico to sleep.

/**
 * Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *
 * Modified 29 Dec 2021
 *   Add on board LED indication of program state.
 */

#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/sleep.h"

#include "hardware/rtc.h"

static bool awake;
static const uint LED_PIN = PICO_DEFAULT_LED_PIN;

static void sleep_callback(void) {
    awake = true;
}

static void rtc_sleep(void) {
// Start on Friday 5th of June 2020 15:45:00
    datetime_t t = {
            .year  = 2020,
            .month = 06,
            .day   = 05,
            .dotw  = 5, // 0 is Sunday, so 5 is Friday
            .hour  = 15,
            .min   = 45,
            .sec   = 00
    };

// Alarm 10 seconds later
    datetime_t t_alarm = {
            .year  = 2020,
            .month = 06,
            .day   = 05,
            .dotw  = 5, // 0 is Sunday, so 5 is Friday
            .hour  = 15,
            .min   = 45,
            .sec   = 10
    };

// Start the RTC
    rtc_init();
    rtc_set_datetime(&t);

// LED OFF to indicate in sleep mode
    gpio_put(LED_PIN, 0);

// start sleep, wake after RTC time interval
    sleep_goto_sleep_until(&t_alarm, &sleep_callback);
 
}

int main() {
// setup on board LED
    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);
    gpio_put(LED_PIN, 1);

// load standard IO and wait 2 seconds, LED ON
    stdio_init_all();
    sleep_ms(2000);

// Flash LED OFF then ON, 1 second each
    gpio_put(LED_PIN, 0);
    sleep_ms(1000);
    gpio_put(LED_PIN, 1);
    sleep_ms(1000);

    sleep_run_from_xosc();

    awake = false;

// start sleep cycle, LED should be OFF while in sleep
    rtc_sleep();

// set LED ON to indicate end of sleep cycle
// if LED does not come on after about 10 seconds, indicates program error
    if (awake) {
        gpio_put(LED_PIN, 1);
    } else {
        gpio_put(LED_PIN, 0);
    }
    sleep_ms(2000);

    return 0;
}

Make file for the program code.

# Set minimum required version of CMake
cmake_minimum_required(VERSION 3.12)
#include build functions from Pico SDK
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
# We also need PICO EXTRAS
include($ENV{PICO_EXTRAS_PATH}/external/pico_extras_import.cmake)

# Set name of project (as PROJECT_NAME) and C/C++ Standards
project(sleepy C CXX ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Creates a pico-sdk subdirectory in our project for the libraries
pico_sdk_init()
# point out the CMake, where to find the executable source file
add_executable(${PROJECT_NAME}
        main.c
)
# create map/bin/hex/uf2 files.
pico_add_extra_outputs(${PROJECT_NAME})
# Pull in our pico_stdlib which pulls in commonly used features (gpio, timer-delay etc)
target_link_libraries(${PROJECT_NAME}
            pico_stdlib
            pico_sleep
)
# enable usb output, disable uart output
pico_enable_stdio_usb(${PROJECT_NAME} 1)
pico_enable_stdio_uart(${PROJECT_NAME} 0)

# create map/bin/hex/uf2 file etc.
pico_add_extra_outputs(${PROJECT_NAME})

# add url via pico_set_program_url
#example_auto_set_url(${PROJECT_NAME})

Make file for the sleep library, changed from the one currently in the Pico-Extras.

if (NOT TARGET pico_sleep)
    add_library(pico_sleep INTERFACE)

    target_sources(pico_sleep INTERFACE
            ${CMAKE_CURRENT_LIST_DIR}/sleep.c
    )

    target_include_directories(pico_sleep INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include)
    target_link_libraries(pico_sleep INTERFACE hardware_clocks hardware_rosc hardware_rtc)
endif()

Regards
Jim

3 Likes

Success with hello_dormant, 0.9mA and surprise, the code starts again where it was stopped.
This is awesome; not as low power as the ATMEGA328P, but will maximise battery life.

My project has 4xAAA NiMH cells rated at 900mAH.
When running should last about 10 hours; when Dormant should last about a month.

Not perfect but really happy with how Visual Studio Code work and the reduced power consumption.

Cheers
Jim

5 Likes

Awesome work Jim! :slight_smile: Glad to see it’s working.

What was the problem?

2 Likes

@Oliver33 Problem was me not understanding enough of how the hardware_clock library in the C++ SDK worked. And then examining sleep.c line by line and understanding what each line was doing. dormant mode pretty much turns off all clocks, so nothing works any more.

In this situation it is hard to determine if the program is actually doing something or not. I used the on board LED and a flash sequence to give me confidence the program was stepping through correctly. Then current measurements would be right. If I have no confidence in the program running as it is supposed to, any current measurement is meaningless. Being able to duplicate the situation again and again also gives confidence.

I understand a little more how to turn various parts of the the Pi Pico off by disabling the clock to it. ie USB, ADC, RTC, Peripherals. Meaning if you are after power saving and not using any of these functions they can be turned off. You could reduce the micro clock to reduce power, you could use the ring oscilator rather than the external crystal. etc

All of this seems to be built into hardware_clock library making it easy to use.

Still got some way to go in testing all of this, but gives me a lot of confidence in the Pi Pico and its usefulness as an embedded micro device.

Regards
Jim

PS The RP2040 datasheet lists 196 registers for the clocks, pretty complex. It also points to the C++ SDK routines making it easy to use. Would be nice if something like this was added to MicroPython.

4 Likes

Hmm, yeah I did some digging. Seems there’s a deep_sleep() function in Micropython for the ESP32 and ESP8266:

https://docs.micropython.org/en/latest/esp8266/tutorial/powerctrl.html#deep-sleep-mode

I found this little micropython library for entering dormant mode on the Pico / RP2040:

4 Likes

@Oliver33 The GitHub low power routines do NOT work correctly. The code sets the Pico into dormant mode but you can never get it out of dormant mode, no matter what you do with the GPIO pin.

Essentially the Pico locks up; the RP2040 datasheet mentions the process you need to follow to activate and deactivate Dormant mode. If this is not followed the Pico will lock up or enter an unknown state.

The GitHub code does nothing with the clocks, this is the problem.

So using the Pico C SDK sleep.c routines as an example I wrote Python code to make it work, and used @micropython.asm_thumb routines to read and write the Pico registers.

My code is pretty messy; but the Pico now enters Dormant mode and leaves it when I activate a GPIO switch. A while loop is used to re-enter Dormant mode after a few seconds. The on board LED is flashed to indicate Dormant mode about to activate. The process can be duplicated as many times as you want. The current drain is 800uA when in Dormant mode.

The code sets up the clocks for Dormant mode and turns off all peripheral clocks before entering Dormant mode. I now need to reactivate some peripheral clocks for my project. Once the code is tidied up, tested and debug I will post here.

I intend to submit the project when its ready.

Cheers
Jim

4 Likes

Awesome, nice work Jim! 800µA is pretty acceptable for battery powered project :slight_smile: That’ll probably get you about 2 months on a 1000mAh LiPo.

3 Likes

Update:
Project uses Pi Pico, PiicoDev CAP1203 & PiicoDev SSD1306.

Measured currents with program running.
19.5mA waiting sensor input, 25mA when sensor touched and LCD on.
The Pico will be driving a relay which draws about 75mA; so total will be about 100mA.

Measured currents in Dormant mode.
Pi Pico = 0.3mA
Pi Pico, CAP1203 = 0.7mA (each sensor scan = 120uA x 3 = 360uA)
Pi Pico, CAP1203, SSD1306 = 2.2mA

Set SSD1306 to poweroff before Dormant mode.
Pi Pico, CAP1203, SSD1306 = 1.4mA

The measured current of 0.3mA is interseting using MicroPython; I originally measured 0.8mA using the C SDK. Turning the clocks off as per sleep.c does not seem to make much difference in Dormant mode, probably would have a greater effect in LightSleep mode.

Regards
Jim

4 Likes

Hi Jim,

This is really awesome info!! Good to see that something like a wall-mounted sensor unit datalogger or control panel for something that utilises those Piicodev bits will last for ages.

Got any plans for where this low power hardware will go project-wise?

-James

3 Likes

Hit a brick wall recently.
Pico enters Dormant mode; wakes when a CAP Touch sensor is activated; uses the INT pin from the board to trigger a low level interrupt, which wakes the Pico. Seemed to work nicely, but every so often it would not wake at all.

Thought it might be the settings in the CAP Touch sensor, NO it’s working fine.
Tried using a switch to provide a definite GND level from a high level, no better.

Don’t really know where to go from here. Have tried everything I can think of, read through the datasheet extensively, checked the software again and again. It should not be doing this, its like there is an intermittent fault in the Pico itself.

Tried different pins, same result. You think its working and you have solved it only for it to not wake up again. Not related to time can work well for a number of cycles and then stops, can remain dormant for a long time and wake, or not as the case maybe.

I have 4 Pico boards, will try each, will also try wake from RTC and wake on pin high.

Posted here just as an update.

Regards
Jim

3 Likes

Some things to consider.

Good, long, wait between last, disabling-instruction, and actually enteting low power state, to ensure no unexpected, or undoccumented internal process is not quite ready for bed.

The duration and quality of the wakeup signal - nice and square, debounced, and of long duration.

Consider testing with all IO removed, including any long leads, in case activity, or current flow, is disturbing rest.

I’d be suspicious of these if there is a flaw in low power wake.

4 Likes

@Gregory128110 Agree, all good points.

Racking my brains trying to think what could be causing what I am observing.
Thought occurred, maybe the software is getting lost or corrupted or something when placed in Dormant mode. I am using MicroPython in the Pi Pico and using def() calls like the C code in the SDK. Of course compiled C code is assembler instructions. Running Python script is very different.

Changed the Python code so the register manipulation occurs all within the one def() function. The clocks are changed as the last thing before activating Dormant mode and reset back as the first thing after exiting Dormant mode.

Waking using GPIO and switch has not failed yet.

Waking using the CAP Touch sensor Interrupt has not failed yet.

Cheers
Jim

PS This has been driving me crazy the last few days.

3 Likes

This is the Python code I have used in my project to put the Pi Pico to sleep.
It has worked all day whenever I want to wake it. Every time it does what its supposed to do.
Seemed to work best when the register manipulation is done in just one def(), otherwise it gets lost on power up.

Pi PIco alone in sleep uses about 0.3mA.

Cheers
Jim

################################################################################
#
#			Pi Pico - Dormant function
#
#	Use these functions to put the Pico to sleep.
#	Power consumption drops to about 0.3mA.
#
# 	def dormant_until_pin(gpio_pin, edge=True, level=True):
#
#	gpio_pin - the GPIO pin interrupt that will wake the Pico
#	edge =  True  - triggers on an edge
#	edge =  False - triggers on a level
#	level = True  - rising edge or High level
#	level = False - falling edge or Low level
#
#   Created 10 Jan 2022
#  
#################################################################################
# Address & Register definitions
##############################################################################################
# GPIO Register start address, GPIO pin determines actual register
#    calculated by int(gpio / 8) * 4
IO_BANK0_BASE = 0x40014000
# Register offsets
IO_BANK0_INTR0 = 0x0f0
IO_BANK0_DORMANT_WAKE_INTE0 = 0x160
# Bit determines type of interrupt, 4 bits per register, 8 registers in each 32 bit word
#     Bit position for GPIO is calculated by event << 4 * (gpio % 8)
#         event is one of 4 options below
LEVEL_LOW =  0x00000001
LEVEL_HIGH = 0x00000002
EDGE_FALL =  0x00000004
EDGE_RISE =  0x00000008

# XOSC Register start address
XOSC_BASE = 0x40024000
XOSC_DORMANT = 0x08
XOSC_STATUS = 0x04
# Values to write to set dormant mode, wake value is set by default
XOSC_DORMANT_VALUE = 0x636f6d61
XOSC_WAKE_VALUE = 0x77616b65
# bit to check indicating XOSC is stable
XOSC_STATUS_STABLE_BIT = 0x80000000
# ROSC Registers start address
ROSC_BASE = 0x40060000

# Clock Registers start address
CLOCKS_BASE = 0x40008000
# Register offsets
CLK_REF_CTRL = 0x30
CLK_SYS_CTRL = 0x3C
CLK_PERI_CTRL = 0x48
CLK_USB_CTRL = 0x54
CLK_ADC_CTRL = 0x60
CLK_RTC_CTRL = 0x6C
PLL_SYS_BASE = 0x40028000
PLL_USB_BASE = 0x4002c000
PLL_PWR = 0x4

##############################################################################################
# Assember routines to directly access registers
##############################################################################################
@micropython.asm_thumb
def _read_bits(r0):
    ldr(r0, [r0, 0])

@micropython.asm_thumb
def _write_bits(r0, r1):
    str(r1, [r0, 0])

##############################################################################################
# Set dormant mode and activate
#     Found it better to put all the register manipulation in the same Python def()
##############################################################################################
def dormant_until_pin(gpio_pin, edge=True, level=True):
# convert edge and level to event
    if not edge and not level:
        event = LEVEL_LOW
    if not edge and level:
        event = LEVEL_HIGH
    if edge and not level:
        event = EDGE_FALL
    if edge and level:
        event = EDGE_RISE
#check for valid gpio
    if gpio_pin < 0 or gpio_pin > 27:
        raise RuntimeError("Invalid value for pin " + str(gpio_pin) + " (expect int between 0 and 27)")
# clear interrrupts
    _write_bits(IO_BANK0_BASE + IO_BANK0_INTR0 + int(gpio_pin / 8) * 4, event << 4 * (gpio_pin % 8))
# Enable Wake-up from GPIO IRQ
    _write_bits(IO_BANK0_BASE + IO_BANK0_DORMANT_WAKE_INTE0 + int(gpio_pin / 8) * 4, event << 4 * (gpio_pin % 8))
# Setup clocks for dormant
    _write_bits(CLOCKS_BASE + CLK_REF_CTRL, 0x02)
    _write_bits(CLOCKS_BASE + CLK_SYS_CTRL, 0x00)
    _write_bits(CLOCKS_BASE + CLK_USB_CTRL, 0x00)
    _write_bits(CLOCKS_BASE + CLK_ADC_CTRL, 0x00)
    _write_bits(CLOCKS_BASE + CLK_RTC_CTRL, 0x60)
    _write_bits(CLOCKS_BASE + CLK_PERI_CTRL, 0x00)
    _write_bits(PLL_USB_BASE + PLL_PWR, 0x2D)
    _write_bits(ROSC_BASE, 0xD1EAA0)
# go dormant
    _write_bits(XOSC_BASE + XOSC_DORMANT, XOSC_DORMANT_VALUE)
####################################
# execution stops here
####################################
# Normalise clocks
    _write_bits(CLOCKS_BASE + CLK_REF_CTRL, 0x02)
    _write_bits(CLOCKS_BASE + CLK_SYS_CTRL, 0x01)
    _write_bits(CLOCKS_BASE + CLK_USB_CTRL, 0x800)
    _write_bits(CLOCKS_BASE + CLK_ADC_CTRL, 0x800)
    _write_bits(CLOCKS_BASE + CLK_RTC_CTRL, 0x800)
    _write_bits(CLOCKS_BASE + CLK_PERI_CTRL, 0x800)
    _write_bits(PLL_USB_BASE + PLL_PWR, 0x04)
    _write_bits(ROSC_BASE, 0xfffaa0)
# Check for stable XOSC after wake up
    while not _read_bits(XOSC_BASE + XOSC_STATUS) & XOSC_STATUS_STABLE_BIT:
         pass
# clear interrupts
    _write_bits(IO_BANK0_BASE + IO_BANK0_INTR0 + int(gpio_pin / 8) * 4, event << 4 * (gpio_pin % 8))

#################################################################################
3 Likes

Brilliant!

3 Likes

Hey @James46717 , sorry for necro-ing the thread, but I am the folk behind https://github.com/tomjorquera/pico-micropython-lowpower-workaround, and someone just pointed me to this topic.

I really like your version of the micropython code which handle clocks properly. Do you mind if I steal it back in the lib to share the improvement around? Hopefully it will help fix up issues people such as you encountered.

(strangely I do not have these issues myself when I test thing on my picos).