Awaking the Raspberry Pico from deep sleep

The Raspberry Pico was released in Jan 2021, and retails for around $4. It has a Dual-core Arm Cortex-M0+ processor (RP2040), flexible clock running up to 133 MHz and 264KB internal RAM.

This is a short overview of how you might reduce the power consumption of your Pico to a very low level, around 1.3mA at 25c according to the official datasheet.

This is useful for periodically performing some work, consuming minimal power whilst we are sleeping.

Sleeping is fairly simple, but there were a few extra steps required to bring the Pico back to full functionality once it’s awake.

Choosing a sleep mode

There appear to be two different sleep modes, a dormant mode and a sleep mode. Read more about them from a programming perspective here in the 2.11.5. Programmer’s Model section.

The dormant mode uses even less power 0.8mA at 25c, however it appears to require the use of an external trigger to bring the Pico back out of sleep.

To use dormant mode, they use the following function in their example:

// Go to sleep until we see a high edge on GPIO 10
sleep_goto_dormant_until_edge_high(10);

If you want to purely rely on the internal clock of the Pico to trigger it to awaken, we’re going to have to use the sleep mode, using slightly more power.

Example sleep code

Lets take a look then at the sleep example from the pico-playground:

int main() {
    stdio_init_all();
    printf("Hello Sleep!\n");

    printf("Switching to XOSC\n");

    // Wait for the fifo to be drained so we get reliable output
    uart_default_tx_wait_blocking();

    // UART will be reconfigured by sleep_run_from_xosc
    sleep_run_from_xosc();

    printf("Switched to XOSC\n");

    awake = false;

    //At this point the pico will sleep
    rtc_sleep();

    //We won't hit this part of the code until the 
    //time set in rtc_sleep() is reached. 
    // Make sure we don't wake
    while (!awake) {
        printf("Should be sleeping\n");
    }

    return 0;
}

The above makes a call to rtc_sleep() which sets everything up and then sleeps for 10 seconds:

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);

    printf("Sleeping for 10 seconds\n");
    uart_default_tx_wait_blocking();

    sleep_goto_sleep_until(&t_alarm, &sleep_callback);
}

All fairly straight forward, and if you run this it works as expected. This line:

printf("Should be sleeping\n");

Is never printed as by the time we reach this point in the code, the sleep callback is triggered on awaking, setting awake to true:

static void sleep_callback(void) {
    printf("RTC woke us up\n");
    awake = true;
}

Fantastic! All sorted?

Unfortunately this is not the whole story, we don’t wake up in a great state. Though at first everything seems fine.

Lets say we expand on the code in the example above, and we try to sleep again using the regular sleep_ms method the Pico SDK provides:

    //We won't hit this part of the code until the 
    //time set in rtc_sleep() is reached. 
    // Make sure we don't wake
    while (!awake) {
        printf("Should be sleeping\n");
    }

    printf("switch on the onboard LED\n");
    uart_default_tx_wait_blocking();
    gpio_put(LED_PIN, 1);

    printf("Sleep from sleep_ms\n");
    uart_default_tx_wait_blocking();

    //We hang at this point forever
    sleep_ms(2000);

    printf("Switch off LED\n");
    uart_default_tx_wait_blocking();
    gpio_put(LED_PIN, 0);

    return 0;

What happens is we hang at the sleep_ms command.

What’s going on here?

Lets follow through the function calls rtc_sleep is making and see what’s being changed to allow us to enter this deep sleep state. One of the functions we call is sleep_run_from_xosc(), this sets up the Pico to run from the internal Crystal Oscillator.

Following that through we end up in this area of the code:

    // CLK SYS = CLK_REF
    clock_configure(clk_sys,
                    CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF,
                    0, // Using glitchless mux
                    src_hz,
                    src_hz);

    // CLK USB = 0MHz
    clock_stop(clk_usb);

    // CLK ADC = 0MHz
    clock_stop(clk_adc);

So it seems we’re reconfiguring the system clock, and stopping a few of the other clocks. Could that be why we’re not able to sleep correctly later? Thankfully an example in the SDK docs gives us a away to check what the internal clocks are set to.

We can print these values out before and after sleeping, and see how it looks.

Disabled clocks

Before sleeping:

pll_sys  = 125001kHz
pll_usb  = 48000kHz
rosc     = 4689kHz
clk_sys  = 125000kHz
clk_peri = 125000kHz
clk_usb  = 48000kHz
clk_adc  = 48000kHz
clk_rtc  = 47kHz

After sleeping:

pll_sys  = 0kHz
pll_usb  = 0kHz
rosc     = 0kHz
clk_sys  = 12000kHz
clk_peri = 12000kHz
clk_usb  = 0kHz
clk_adc  = 0kHz
clk_rtc  = 47kHz

That’s probably not helping. There is actually a clocks_init function provided by the SDK, however when trying to call that after sleeping, it will just hang.

By creating a new function, and picking out some of the commands from clocks_init, some of the clocks could be reset.

pll_sys  = 125000kHz
pll_usb  = 48000kHz
rosc     = 0kHz
clk_sys  = 12000kHz
clk_peri = 12000kHz
clk_usb  = 48000kHz
clk_adc  = 48000kHz
clk_rtc  = 47kHz

However, trying to reset the system clock would seem to hang:

clock_configure(clk_sys,
                    CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLKSRC_CLK_SYS_AUX,
                    CLOCKS_CLK_SYS_CTRL_AUXSRC_VALUE_CLKSRC_PLL_SYS,
                    125 * MHZ,
                    125 * MHZ);

As the ring oscillator is still disabled, lets see if we can bring that back to a good state. It’s being disabled in this part of the code. Which does the following:

void rosc_disable(void) {
    uint32_t tmp = rosc_hw->ctrl;
    tmp &= (~ROSC_CTRL_ENABLE_BITS);
    tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB);
    rosc_write(&rosc_hw->ctrl, tmp);
    // Wait for stable to go away
    while(rosc_hw->status & ROSC_STATUS_STABLE_BITS);
}

As a side note, the SDK has an interesting note around disabling the ring oscillator before switching the system clock to run from another source.

The system clock must be switched to another source before
setting this field to DISABLE otherwise the chip will lock up

Lets try and re-enable the ROSC:

rosc_write(&rosc_hw->ctrl, ROSC_CTRL_ENABLE_BITS);

At this point we can then run clocks_init and it no longer hangs, the clocks are then back to a similar state before sleeping, great!

pll_sys  = 125000kHz
pll_usb  = 48000kHz
rosc     = 4675kHz
clk_sys  = 125000kHz
clk_peri = 125000kHz
clk_usb  = 48000kHz
clk_adc  = 48000kHz
clk_rtc  = 47kHz

We’re still hanging

Unfortunately though, we’re still hanging if we try and call sleep_ms after waking. Let’s go deeper into the code to see what happens when we actually sleep.

sleep_goto_sleep_until(&t_alarm, &sleep_callback);

In this function, all the clocks are being disabled apart from the Real Time Clock (we need this to wake us up):

    // Turn off all clocks when in sleep mode except for RTC
    clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS;
    clocks_hw->sleep_en1 = 0x0;

We’re then enabling deep sleeping on the processor:

    uint save = scb_hw->scr;
    // Enable deep sleep at the proc
    scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS;

So it’s likely we’re going to need to reverse these settings as well to bring everything back to the pre-sleep state. Lets create a function to bring everything back to the good state for us:

void recover_from_sleep(uint scb_orig, uint clock0_orig, uint clock1_orig){

    //Re-enable ring Oscillator control
    rosc_write(&rosc_hw->ctrl, ROSC_CTRL_ENABLE_BITS);

    //reset procs back to default
    scb_hw->scr = scb_orig;
    clocks_hw->sleep_en0 = clock0_orig;
    clocks_hw->sleep_en1 = clock1_orig;

    //reset clocks
    clocks_init();
    stdio_init_all();

    return;
}

Bringing it all together then we end up with the following:

int main() {

    const uint LED_PIN = 25;
    stdio_init_all();

    gpio_init(LED_PIN);
    gpio_set_dir(LED_PIN, GPIO_OUT);

    //save values for later
    uint scb_orig = scb_hw->scr;
    uint clock0_orig = clocks_hw->sleep_en0;
    uint clock1_orig = clocks_hw->sleep_en1;

    printf("Hello Sleep!\n");

    printf("Switching to XOSC\n");

    // Wait for the fifo to be drained so we get reliable output
    uart_default_tx_wait_blocking();

    // UART will be reconfigured by sleep_run_from_xosc
    sleep_run_from_xosc();

    printf("Switched to XOSC\n");

    awake = false;

    //At this point the pico will sleep
    rtc_sleep();

    //We won't hit this part of the code until the 
    //time set in rtc_sleep() is reached. 
    // Make sure we don't wake
    while (!awake) {
        printf("Should be sleeping\n");
    }

    //reset processor and clocks back to defaults
    recover_from_sleep(scb_orig, clock0_orig, clock1_orig);    
    
    printf("switch on the onboard LED\n");
    uart_default_tx_wait_blocking();
    gpio_put(LED_PIN, 1);

    //We no longer hang here!
    sleep_ms(2000);

    printf("Switch off LED\n");
    uart_default_tx_wait_blocking();
    gpio_put(LED_PIN, 0);

    return 0;
}

Finally we no longer hang when calling sleep_ms after waking.

Full example with build instructions can be seen here.

Hopefully this has been somewhat interesting and perhaps will help someone else to wake their Pico after deep sleeping.