Skip to main content

esp_rtos/
sleep.rs

1//! Power management utilities.
2
3#[cfg(multi_core)]
4use esp_hal::{peripherals::CPU_CTRL, system::Cpu, system::CpuControl};
5use esp_hal::{
6    peripherals::LPWR,
7    rtc_cntl::{
8        WakeLock,
9        sleep::{LowPower, RtcSleepConfig},
10    },
11    time::{Duration, Instant},
12};
13
14use crate::{SCHEDULER, task::IdleFn};
15#[cfg(multi_core)]
16use crate::{run_queue::RunSchedulerOn, task};
17
18const LIGHT_SLEEP_MIN_US: u64 =
19    esp_config::esp_config_int!(u32, "ESP_RTOS_CONFIG_LIGHT_SLEEP_MIN_US") as u64;
20
21/// Sleep handles.
22pub struct Sleep {
23    /// The handle that allows you to enter deep sleep.
24    #[cfg(sleep_deep_sleep)]
25    pub deep_sleep: DeepSleep,
26
27    /// The idle hook to use for light sleep.
28    pub light_sleep_hook: IdleFn,
29}
30
31/// A handle you can use to enter deep sleep.
32#[cfg(sleep_deep_sleep)]
33pub struct DeepSleep {
34    lpwr: LPWR<'static>,
35}
36
37#[cfg(sleep_deep_sleep)]
38impl DeepSleep {
39    /// Puts the system into deep sleep.
40    ///
41    /// The sleep ends when one of the wakeup sources that the drivers enabled becomes active. The
42    /// wake from deep sleep resets the chip, so this function does not return.
43    ///
44    /// # Panics
45    ///
46    /// Panics if no wakeup source is enabled, because nothing could end the sleep.
47    pub fn deep_sleep(&mut self) -> ! {
48        let mut lpwr = LowPower::new(self.lpwr.reborrow());
49        lpwr.sleep_deep(RtcSleepConfig::deep())
50    }
51}
52
53/// Creates resources for managing light/deep sleep with `esp-rtos`.
54///
55/// The returned [`Sleep`] struct contains the idle hook and a deep sleep handle,
56/// if deep sleep is supported.
57///
58/// Pass the idle hook to [`start_with_idle_hook`] to enable automatic light sleep.
59///
60/// Each time the scheduler runs out of ready tasks, the hook (with interrupts
61/// disabled) checks that:
62/// - no [`WakeLock`] is held,
63/// - all cores are idle,
64/// - the next wakeup is at least `ESP_RTOS_CONFIG_LIGHT_SLEEP_MIN_US` microseconds away.
65///
66/// If all hold, it calls [`LowPower::sleep_light`] for the next wakeup; otherwise
67/// it falls back to `WFI`. The minimum-residency threshold is configurable via the
68/// `ESP_RTOS_CONFIG_LIGHT_SLEEP_MIN_US` build-time option (default `1000`).
69///
70/// On multi-core chips, the core that commits to sleep hardware-stalls the other core(s)
71/// for the duration of the sleep so their CPU state is frozen and restored coherently,
72/// then thaws them on wakeup.
73///
74/// See [`WakeLock`] for the wake-lock contract that governs when sleeping is safe.
75///
76/// [`start_with_idle_hook`]: crate::start_with_idle_hook
77pub fn configure(lpwr: LPWR<'static>) -> Sleep {
78    Sleep {
79        #[cfg(sleep_deep_sleep)]
80        deep_sleep: DeepSleep { lpwr },
81        light_sleep_hook: auto_light_sleep_hook,
82    }
83}
84
85extern "C" fn auto_light_sleep_hook() -> ! {
86    loop {
87        // ESP32-P4 HP wakeup handling is coordinated by the primary core. If
88        // AppCpu enters sleep after parking ProCpu, an HP peripheral wakeup
89        // such as GPIO cannot resume the parked primary core.
90        #[cfg(all(multi_core, esp32p4))]
91        if Cpu::current() == Cpu::AppCpu {
92            // Kick the other core so that it can put the system to sleep.
93            task::trigger_scheduler(RunSchedulerOn::RunOnCore(Cpu::ProCpu));
94            esp_hal::interrupt::wait_for_interrupt();
95            continue;
96        }
97
98        SCHEDULER.with(|scheduler| {
99            if WakeLock::is_active() {
100                return;
101            }
102
103            #[cfg(multi_core)]
104            {
105                if scheduler.run_queue.has_ready_tasks() {
106                    return;
107                }
108                for cpu in Cpu::all() {
109                    if !scheduler.cpu_idle(cpu) {
110                        return;
111                    }
112                }
113
114                // All cores are ready to sleep. Since we are here in a critical section,
115                // the other core must be waiting for the scheduler lock. We will go to sleep,
116                // and after wakeup the other core will reattempt this check.
117
118                // FIXME: We hardware-stall the other core(s) for the duration of the sleep so
119                // their CPU state is frozen and restored coherently. The other core is frozen
120                // wherever it happens to be - including in the middle of an interrupt handler
121                // that holds a cross-core lock (e.g. the clock tree, peripheral refcount, or
122                // UART locks taken by the light-sleep enter/exit path in `Rtc::sleep`). If that
123                // happens, this core will spin forever trying to take that lock during sleep
124                // prep, because the frozen core can never release it. We accept this (unlikely)
125                // deadlock risk for now rather than ordering all lock-taking work before the
126                // stall.
127            }
128
129            let Some(time_driver) = scheduler.time_driver.as_mut() else {
130                return;
131            };
132            let next_wakeup = time_driver.next_wakeup();
133
134            let mut lpwr = LowPower::new(unsafe { LPWR::steal() });
135
136            // The deadline stays until other code clears it, so each pass writes it, also a pass
137            // that makes no sleep. A deadline from an earlier pass expires, and the
138            // next sleep then returns immediately.
139            if next_wakeup == u64::MAX {
140                lpwr.clear_wakeup_deadline();
141            } else {
142                lpwr.set_wakeup_deadline(Instant::EPOCH + Duration::from_micros(next_wakeup));
143
144                if next_wakeup.saturating_sub(crate::now()) < LIGHT_SLEEP_MIN_US {
145                    return;
146                }
147            }
148
149            // We have committed to sleeping. Park (hardware-stall) the other core(s) so their
150            // CPU state is frozen and restored coherently across the sleep, then enter light
151            // sleep, then thaw them.
152            cfg_select! {
153                multi_core => {
154                    let mut cpu_control = CpuControl::new(unsafe { CPU_CTRL::steal() });
155                    for cpu in Cpu::other() {
156                        if scheduler.active_cores.contains(cpu) {
157                            unsafe { cpu_control.park_core(cpu) };
158                            // FIXME: this is insufficient when we power down the CPU - we will
159                            // need to force the other core to be parked in a known place, saving
160                            // its state so we can restore it after wakeup.
161                        }
162                    }
163                }
164                _ => {}
165            }
166
167            // The driver of each other wakeup source enables it. A listening pin wakes the chip
168            // because it listens, and this hook cannot know which pins listen. If no source is
169            // enabled, the call refuses the sleep and returns immediately. The code then reaches
170            // the same `WFI` that this hook would select.
171            lpwr.sleep_light(RtcSleepConfig::default());
172
173            // The alarm timer was gated during light sleep, so its pre-armed alarm is
174            // stale. Force a re-arm against the restored time base so the tick handler
175            // fires promptly and drains the timer queue.
176            time_driver.rearm(crate::now());
177
178            // Trigger the scheduler on the other core to prevent it from putting
179            // the system back to sleep immediately.
180            #[cfg(multi_core)]
181            for cpu in Cpu::other() {
182                if scheduler.active_cores.contains(cpu) {
183                    cpu_control.unpark_core(cpu);
184                    task::trigger_scheduler(RunSchedulerOn::RunOnCore(cpu));
185                }
186            }
187        });
188
189        esp_hal::interrupt::wait_for_interrupt();
190    }
191}