Skip to content

Commit

Permalink
Merge pull request #701 from andrewh42/wake-from-dormant
Browse files Browse the repository at this point in the history
  • Loading branch information
jannic authored Mar 2, 2024
2 parents 556dddd + 0b274b6 commit 1000a10
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 35 deletions.
5 changes: 5 additions & 0 deletions rp2040-hal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ required-features = ["critical-section-impl"]
name = "dht11"
required-features = ["critical-section-impl"]

[[example]]
# dormant_sleep example uses cortex-m-rt::interrupt, need rt feature for that
name = "dormant_sleep"
required-features = ["rt", "critical-section-impl"]

[[example]]
name = "gpio_in_out"
required-features = ["critical-section-impl"]
Expand Down
298 changes: 298 additions & 0 deletions rp2040-hal/examples/dormant_sleep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
//! # DORMANT low-power mode example
//!
//! This application demonstrates how to enter and exit the RP2040's lowest-power DORMANT mode
//! where all clocks and PLLs are stopped.
//!
//! Pulling GPIO 14 low (e.g. via a debounced momentary-contact button) alternately wakes the
//! RP2040 from DORMANT mode and a regular WFI sleep. A LED attached to GPIO 25 (the onboard LED
//! on the Raspberry Pi Pico) pulses once before entering DORMANT mode and twice before entering WFI sleep.
//!
//! Note: DORMANT mode breaks the debug connection. You may need to power cycle while pressing the
//! BOOTSEL button to regain debug access to the pico.
//!
//! It may need to be adapted to your particular board layout and/or pin assignment.
//!
//! See the `Cargo.toml` file for Copyright and license details.
#![no_std]
#![no_main]

#[allow(unused_imports)]
use panic_halt as _;

use rp2040_hal as hal;

use core::{cell::RefCell, ops::DerefMut};

use critical_section::Mutex;

use embedded_hal::digital::StatefulOutputPin;

use fugit::RateExtU32;

use hal::{
clocks::{ClockError, ClocksManager, InitError, StoppableClock},
gpio,
gpio::{Interrupt::EdgeLow, Pins},
pac,
pac::{interrupt, CLOCKS, PLL_SYS, PLL_USB, RESETS, XOSC},
pll::{
common_configs::{PLL_SYS_125MHZ, PLL_USB_48MHZ},
setup_pll_blocking, start_pll_blocking, Disabled, Locked, PhaseLockedLoop,
},
rosc::RingOscillator,
sio::Sio,
watchdog::Watchdog,
xosc::{setup_xosc_blocking, CrystalOscillator, Stable, Unstable},
Clock,
};

use nb::block;

type ClocksAndPlls = (
ClocksManager,
CrystalOscillator<Stable>,
PhaseLockedLoop<Locked, PLL_SYS>,
PhaseLockedLoop<Locked, PLL_USB>,
);

type RestartedClockAndPlls = (
CrystalOscillator<Stable>,
PhaseLockedLoop<Locked, PLL_SYS>,
PhaseLockedLoop<Locked, PLL_USB>,
);

/// The button input.
type ButtonPin = gpio::Pin<gpio::bank0::Gpio14, gpio::FunctionSioInput, gpio::PullUp>;

/// Devices shared between the foreground code and interrupt handlers.
static GLOBAL_DEVICES: Mutex<RefCell<Option<ButtonPin>>> = Mutex::new(RefCell::new(None));

/// The linker will place this boot block at the start of our program image. We
/// need this to help the ROM bootloader get our code up and running.
/// Note: This boot block is not necessary when using a rp-hal based BSP
/// as the BSPs already perform this step.
#[link_section = ".boot2"]
#[used]
pub static BOOT2: [u8; 256] = rp2040_boot2::BOOT_LOADER_GENERIC_03H;

/// External high-speed crystal on the Raspberry Pi Pico board is 12 MHz. Adjust
/// if your board has a different frequency.
const XTAL_FREQ_HZ: u32 = 12_000_000u32;

#[rp2040_hal::entry]
fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let sio = Sio::new(pac.SIO);

// Configure the clocks
let (mut clocks, mut xosc, mut pll_sys, mut pll_usb) = init_clocks_and_plls(
XTAL_FREQ_HZ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();

// Disable ring oscillator to maximise power savings - optional
let rosc = RingOscillator::new(pac.ROSC).initialize();
rosc.disable();

// Set the pins to their default state
let pins = Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);

// Configure GPIO 25 as an output
let mut led_pin = pins.gpio25.into_push_pull_output();

// Configure GPIO 14 as an input that wakes the RP2040 from a sleep state
let button_pin = pins.gpio14.reconfigure();
button_pin.set_dormant_wake_enabled(EdgeLow, true);
button_pin.set_interrupt_enabled(EdgeLow, true);

critical_section::with(|cs| {
GLOBAL_DEVICES.borrow(cs).replace(Some(button_pin));
});

unsafe {
pac::NVIC::unmask(pac::Interrupt::IO_IRQ_BANK0);
}

let mut use_dormant = true;
loop {
if use_dormant {
pulse(&mut led_pin, 1);

let (disabled_pll_sys, disabled_pll_usb) =
prepare_clocks_and_plls_for_dormancy(&mut xosc, &mut clocks, pll_sys, pll_usb);

// Stop the crystal oscillator and enter the RP2040's dormant state
let unstable_xosc = unsafe { xosc.dormant() };

match restart_clocks_and_plls(
&mut clocks,
unstable_xosc,
disabled_pll_sys,
disabled_pll_usb,
&mut pac.RESETS,
) {
Ok((stable_xosc, stable_pll_sys, stable_pll_usb)) => {
xosc = stable_xosc;
pll_sys = stable_pll_sys;
pll_usb = stable_pll_usb;
}
Err(_) => {
panic!();
}
}

// Clear dormant wake interrupt status to enable wake next time
critical_section::with(|cs| {
let mut global_devices = GLOBAL_DEVICES.borrow(cs).borrow_mut();
if let Some(ref mut trigger_pin) = global_devices.deref_mut() {
trigger_pin.clear_interrupt(EdgeLow);
} else {
panic!();
};
});
} else {
pulse(&mut led_pin, 2);

// Enter the regular RP2040 sleep state: clocks and PLLs stay running
cortex_m::asm::wfi();
}

use_dormant = !use_dormant;
}
}

/// Pulse an LED-connected pin the specified number of times.
fn pulse<P: StatefulOutputPin>(pin: &mut P, count: u32) {
const LED_PULSE_CYCLES: u32 = 2_000_000;

for i in 0..count * 2 {
let _ = pin.toggle();
// 1:10 duty cycle
cortex_m::asm::delay(LED_PULSE_CYCLES + (i % 2) * 9 * LED_PULSE_CYCLES);
}
}

/// Initialize clocks and PLLs in much the same way as rp2040-hal::clocks::init_clocks_and_plls().
/// Returns the crystal oscillator and the PLLs so we can reconfigure them later.
fn init_clocks_and_plls(
xosc_crystal_freq: u32,
xosc_dev: XOSC,
clocks_dev: CLOCKS,
pll_sys_dev: PLL_SYS,
pll_usb_dev: PLL_USB,
resets: &mut RESETS,
watchdog: &mut Watchdog,
) -> Result<ClocksAndPlls, InitError> {
let xosc = setup_xosc_blocking(xosc_dev, xosc_crystal_freq.Hz()).unwrap();

// Configure watchdog tick generation to tick over every microsecond
watchdog.enable_tick_generation((xosc_crystal_freq / 1_000_000) as u8);

let mut clocks = ClocksManager::new(clocks_dev);

let pll_sys = setup_pll_blocking(
pll_sys_dev,
xosc.operating_frequency(),
PLL_SYS_125MHZ,
&mut clocks,
resets,
)
.map_err(InitError::PllError)?;
let pll_usb = setup_pll_blocking(
pll_usb_dev,
xosc.operating_frequency(),
PLL_USB_48MHZ,
&mut clocks,
resets,
)
.map_err(InitError::PllError)?;

clocks
.init_default(&xosc, &pll_sys, &pll_usb)
.map_err(InitError::ClockError)?;

Ok((clocks, xosc, pll_sys, pll_usb))
}

/// Switch clocks to the crystal oscillator or disable them as appropriate, and stop PLLs so
/// that we're ready to go dormant.
fn prepare_clocks_and_plls_for_dormancy(
xosc: &mut CrystalOscillator<Stable>,
clocks: &mut ClocksManager,
pll_sys: PhaseLockedLoop<Locked, PLL_SYS>,
pll_usb: PhaseLockedLoop<Locked, PLL_USB>,
) -> (
PhaseLockedLoop<Disabled, PLL_SYS>,
PhaseLockedLoop<Disabled, PLL_USB>,
) {
// switch system clock from pll_sys to xosc so that we can stop the system PLL
nb::block!(clocks.system_clock.reset_source_await()).unwrap();

clocks.usb_clock.disable();
clocks.adc_clock.disable();

clocks
.rtc_clock
.configure_clock(xosc, 46875u32.Hz())
.unwrap();
clocks
.peripheral_clock
.configure_clock(&clocks.system_clock, clocks.system_clock.freq())
.unwrap();

(pll_sys.disable(), pll_usb.disable())
}

/// Restart the PLLs and start/reconfigure the clocks back to how they were before going dormant.
fn restart_clocks_and_plls(
clocks: &mut ClocksManager,
unstable_xosc: CrystalOscillator<Unstable>,
disabled_pll_sys: PhaseLockedLoop<Disabled, PLL_SYS>,
disabled_pll_usb: PhaseLockedLoop<Disabled, PLL_USB>,
resets: &mut RESETS,
) -> Result<RestartedClockAndPlls, ClockError> {
// Wait for the restarted XOSC to stabilise
let stable_xosc_token = block!(unstable_xosc.await_stabilization()).unwrap();
let xosc = unstable_xosc.get_stable(stable_xosc_token);

let pll_sys = start_pll_blocking(disabled_pll_sys, resets).unwrap();
let pll_usb = start_pll_blocking(disabled_pll_usb, resets).unwrap();

clocks
.init_default(&xosc, &pll_sys, &pll_usb)
.map(|_| (xosc, pll_sys, pll_usb))
}

#[interrupt]
fn IO_IRQ_BANK0() {
critical_section::with(|cs| {
let mut global_devices = GLOBAL_DEVICES.borrow(cs).borrow_mut();
if let Some(ref mut button_pin) = global_devices.deref_mut() {
// Check if the interrupt source is from the push button going from high-to-low.
// Note: this will always be true in this example, as that is the only enabled GPIO interrupt source
if button_pin.interrupt_status(EdgeLow) {
// Our interrupt doesn't clear itself.
// Do that now so we don't immediately jump back to this interrupt handler.
button_pin.clear_interrupt(EdgeLow);
}
} else {
panic!();
};
});
}

// End of file
10 changes: 5 additions & 5 deletions rp2040-hal/src/clocks/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,14 @@ macro_rules! clock {
self.set_div(div);
}

// If switching a glitchless slice (ref or sys) to an aux source, switch
// away from aux *first* to avoid passing glitches when changing aux mux.
// Assume (!!!) glitchless source 0 is no faster than the aux source.
nb::block!(self.reset_source_await()).unwrap();


// Set aux mux first, and then glitchless mux if this self has one
let token = if src.is_aux() {
// If switching a glitchless slice (ref or sys) to an aux source, switch
// away from aux *first* to avoid passing glitches when changing aux mux.
// Assume (!!!) glitchless source 0 is no faster than the aux source.
nb::block!(self.reset_source_await()).unwrap();

self.set_aux(src);
self.set_self_aux_src()
} else {
Expand Down
33 changes: 31 additions & 2 deletions rp2040-hal/src/pll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,25 @@ impl<D: PhaseLockedLoopDevice> PhaseLockedLoop<Locked, D> {
pub fn operating_frequency(&self) -> HertzU32 {
self.state.frequency
}

/// Shut down the PLL. The returned PLL is configured the same as it was originally.
pub fn disable(self) -> PhaseLockedLoop<Disabled, D> {
let fbdiv = self.device.fbdiv_int().read().fbdiv_int().bits();
let refdiv = self.device.cs().read().refdiv().bits();
let prim = self.device.prim().read();
let frequency = self.state.frequency;

self.device.pwr().reset();
self.device.fbdiv_int().reset();

self.transition(Disabled {
refdiv,
fbdiv,
post_div1: prim.postdiv1().bits(),
post_div2: prim.postdiv2().bits(),
frequency,
})
}
}

/// Blocking helper method to setup the PLL without going through all the steps.
Expand All @@ -293,8 +312,18 @@ pub fn setup_pll_blocking<D: PhaseLockedLoopDevice>(

nb::block!(clocks.reference_clock.reset_source_await()).unwrap();

let initialized_pll =
PhaseLockedLoop::new(dev, xosc_frequency.convert(), config)?.initialize(resets);
start_pll_blocking(
PhaseLockedLoop::new(dev, xosc_frequency.convert(), config)?,
resets,
)
}

/// Blocking helper method to (re)start a PLL.
pub fn start_pll_blocking<D: PhaseLockedLoopDevice>(
disabled_pll: PhaseLockedLoop<Disabled, D>,
resets: &mut RESETS,
) -> Result<PhaseLockedLoop<Locked, D>, Error> {
let initialized_pll = disabled_pll.initialize(resets);

let locked_pll_token = nb::block!(initialized_pll.await_lock()).unwrap();

Expand Down
Loading

0 comments on commit 1000a10

Please sign in to comment.