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}