Skip to main content

ove/
pm.rs

1// Copyright (C) 2026 Kamil Lulko <kamil.lulko@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4//
5// This file is part of oveRTOS.
6
7//! Power management framework for oveRTOS.
8//!
9//! Provides a unified interface for sleep state management, peripheral power
10//! domains, wake source registration, pluggable power policies, and runtime
11//! power statistics.
12//!
13//! The PM subsystem is a singleton — there is one system-wide power state.
14//! Initialise with [`init`] and tear down with [`deinit`].
15
16use crate::bindings;
17use crate::error::{Error, Result};
18
19// SAFETY (module-wide contract for the `unsafe { bindings::ove_*(...) }` FFI
20// calls below): any handle passed to the C API is non-null and refers to a
21// live RTOS object — wrapper constructors establish validity via
22// `Error::from_code`, and `Drop` (or an explicit `deinit`) is the only place
23// a handle is released. Pointer and slice arguments reference caller-owned
24// memory valid for the duration of the call; the C side copies whatever it
25// retains and does not alias them past return (verified against the
26// signatures in `include/ove/*.h`). Blocks that deviate — `transmute`, raw
27// pointer casts from user data, slice reconstruction via `from_raw_parts`,
28// or storing a callback across the FFI boundary — carry their own
29// `// SAFETY:` comment.
30
31// ── Enumerations ────────────────────────────────────────────────────────
32
33/// System power states, ordered by increasing sleep depth.
34#[repr(u32)]
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
36pub enum State {
37    /// Full speed, all clocks running.
38    Active = 0,
39    /// Light sleep, fast wakeup, peripherals on.
40    Idle = 1,
41    /// Deep idle, some peripherals off.
42    Standby = 2,
43    /// Lowest power, RAM retained, slow wakeup.
44    DeepSleep = 3,
45}
46
47/// Wake source types.
48#[repr(u32)]
49#[derive(Debug, Copy, Clone, PartialEq, Eq)]
50pub enum WakeType {
51    /// GPIO pin edge.
52    Gpio = 0,
53    /// Timer expiry.
54    Timer = 1,
55    /// UART RX activity.
56    Uart = 2,
57    /// RTC alarm.
58    Rtc = 3,
59}
60
61/// Peripheral power domain identifiers.
62#[repr(u32)]
63#[derive(Debug, Copy, Clone, PartialEq, Eq)]
64pub enum Domain {
65    Radio = 0,
66    Sensor = 1,
67    Display = 2,
68    Audio = 3,
69    Storage = 4,
70    Comms = 5,
71    User0 = 6,
72    User1 = 7,
73}
74
75/// Transition event type for notification callbacks.
76#[repr(u32)]
77#[derive(Debug, Copy, Clone, PartialEq, Eq)]
78pub enum Event {
79    /// About to enter low-power state.
80    PreSleep = 0,
81    /// Just woke from low-power state.
82    PostWake = 1,
83}
84
85// ── Configuration ───────────────────────────────────────────────────────
86
87/// PM subsystem configuration.
88#[derive(Debug, Copy, Clone)]
89pub struct Cfg {
90    /// Idle ms before ACTIVE to IDLE transition.
91    pub idle_threshold_ms: u32,
92    /// Idle ms before IDLE to STANDBY transition.
93    pub standby_threshold_ms: u32,
94    /// Idle ms before DEEP_SLEEP transition.
95    pub deep_sleep_threshold_ms: u32,
96}
97
98/// Runtime power statistics.
99#[derive(Debug, Copy, Clone)]
100pub struct Stats {
101    /// Cumulative microseconds in each state.
102    pub time_in_state_us: [u64; 4],
103    /// Number of entries per state.
104    pub transition_count: [u32; 4],
105    /// Total tracked runtime in microseconds.
106    pub total_runtime_us: u64,
107    /// Active percentage in hundredths (0..10000).
108    pub active_pct_x100: u32,
109}
110
111// ── Lifecycle ───────────────────────────────────────────────────────────
112
113/// Initialise the PM subsystem.
114///
115/// # Errors
116/// Returns [`Error::InvalidParam`] if `cfg` values are invalid.
117pub fn init(cfg: &Cfg) -> Result<()> {
118    let c_cfg = bindings::ove_pm_cfg {
119        idle_threshold_ms: cfg.idle_threshold_ms,
120        standby_threshold_ms: cfg.standby_threshold_ms,
121        deep_sleep_threshold_ms: cfg.deep_sleep_threshold_ms,
122    };
123    let rc = unsafe { bindings::ove_pm_init(&c_cfg) };
124    Error::from_code(rc)
125}
126
127/// Tear down the PM subsystem and release resources.
128pub fn deinit() {
129    unsafe { bindings::ove_pm_deinit() }
130}
131
132// ── State machine ───────────────────────────────────────────────────────
133
134/// Request an explicit power state transition.
135///
136/// # Errors
137/// Returns [`Error::InvalidParam`] if `state` is out of range.
138pub fn set_state(state: State) -> Result<()> {
139    let rc = unsafe { bindings::ove_pm_set_state(state as bindings::ove_pm_state_t) };
140    Error::from_code(rc)
141}
142
143/// Query the current power state.
144pub fn get_state() -> State {
145    let raw = unsafe { bindings::ove_pm_get_state() };
146    match raw {
147        1 => State::Idle,
148        2 => State::Standby,
149        3 => State::DeepSleep,
150        _ => State::Active,
151    }
152}
153
154/// Report system activity (ISR-safe). Resets the idle timer.
155pub fn activity() {
156    unsafe { bindings::ove_pm_activity() }
157}
158
159// ── Wake sources ────────────────────────────────────────────────────────
160
161/// Register a GPIO wake source.
162///
163/// # Errors
164/// Returns [`Error::NoMemory`] if the wake source table is full.
165pub fn wake_register_gpio(port: u32, pin: u32, edge: u32) -> Result<()> {
166    let mut src: bindings::ove_pm_wake_src = unsafe { core::mem::zeroed() };
167    src.type_ = WakeType::Gpio as bindings::ove_pm_wake_type_t;
168    src.__bindgen_anon_1.gpio.port = port;
169    src.__bindgen_anon_1.gpio.pin = pin;
170    src.__bindgen_anon_1.gpio.edge = edge as bindings::ove_gpio_irq_mode_t;
171    let rc = unsafe { bindings::ove_pm_wake_register(&src) };
172    Error::from_code(rc)
173}
174
175/// Register a timer wake source.
176///
177/// # Errors
178/// Returns [`Error::NoMemory`] if the wake source table is full.
179pub fn wake_register_timer(timeout_ms: u32) -> Result<()> {
180    let mut src: bindings::ove_pm_wake_src = unsafe { core::mem::zeroed() };
181    src.type_ = WakeType::Timer as bindings::ove_pm_wake_type_t;
182    src.__bindgen_anon_1.timer.timeout_ms = timeout_ms;
183    let rc = unsafe { bindings::ove_pm_wake_register(&src) };
184    Error::from_code(rc)
185}
186
187/// Register a UART wake source.
188///
189/// # Errors
190/// Returns [`Error::NoMemory`] if the wake source table is full.
191pub fn wake_register_uart(instance: u32) -> Result<()> {
192    let mut src: bindings::ove_pm_wake_src = unsafe { core::mem::zeroed() };
193    src.type_ = WakeType::Uart as bindings::ove_pm_wake_type_t;
194    src.__bindgen_anon_1.uart.instance = instance;
195    let rc = unsafe { bindings::ove_pm_wake_register(&src) };
196    Error::from_code(rc)
197}
198
199/// Unregister a GPIO wake source.
200///
201/// # Errors
202/// Returns [`Error::NotRegistered`] if the wake source was not found.
203pub fn wake_unregister_gpio(port: u32, pin: u32) -> Result<()> {
204    let mut src: bindings::ove_pm_wake_src = unsafe { core::mem::zeroed() };
205    src.type_ = WakeType::Gpio as bindings::ove_pm_wake_type_t;
206    src.__bindgen_anon_1.gpio.port = port;
207    src.__bindgen_anon_1.gpio.pin = pin;
208    let rc = unsafe { bindings::ove_pm_wake_unregister(&src) };
209    Error::from_code(rc)
210}
211
212// ── Peripheral power domains ────────────────────────────────────────────
213
214/// Increment the reference count for a peripheral power domain.
215///
216/// On the first request (0 to 1), the domain hardware is powered on.
217///
218/// # Errors
219/// Returns [`Error::InvalidParam`] if `domain` is out of range.
220pub fn domain_request(domain: Domain) -> Result<()> {
221    let rc = unsafe { bindings::ove_pm_domain_request(domain as bindings::ove_pm_domain_t) };
222    Error::from_code(rc)
223}
224
225/// Decrement the reference count for a peripheral power domain.
226///
227/// When the count reaches zero, the domain hardware is powered off.
228///
229/// # Errors
230/// Returns [`Error::InvalidParam`] on underflow.
231pub fn domain_release(domain: Domain) -> Result<()> {
232    let rc = unsafe { bindings::ove_pm_domain_release(domain as bindings::ove_pm_domain_t) };
233    Error::from_code(rc)
234}
235
236/// Query the current reference count for a domain.
237///
238/// # Errors
239/// Returns a negative error code if `domain` is invalid.
240pub fn domain_get_refcount(domain: Domain) -> Result<i32> {
241    let rc = unsafe { bindings::ove_pm_domain_get_refcount(domain as bindings::ove_pm_domain_t) };
242    if rc < 0 {
243        Error::from_code(rc)?;
244    }
245    Ok(rc)
246}
247
248// ── Policy ──────────────────────────────────────────────────────────────
249
250/// Raw-pointer variant of [`set_policy`].
251///
252/// # Safety
253/// The callback and `user_data` must remain valid for the lifetime of the
254/// registration. Prefer the safe [`PolicyHandler`]-based [`set_policy`].
255pub unsafe fn set_policy_raw(
256    policy: bindings::ove_pm_policy_fn,
257    user_data: *mut core::ffi::c_void,
258) -> Result<()> {
259    let rc = unsafe { bindings::ove_pm_set_policy(policy, user_data) };
260    Error::from_code(rc)
261}
262
263/// Per-tick context passed to a [`PolicyHandler`].
264#[derive(Debug, Copy, Clone)]
265pub struct PolicyCtx {
266    pub current: State,
267    pub idle_ms: u32,
268    pub next_timeout_ms: u32,
269}
270
271/// A registered power-policy handler bound to a static state cell.
272///
273/// Construct with [`PolicyHandler::new`] and register with [`set_policy`].
274/// The handler must be `'static` (typically declared at module scope with
275/// [`crate::shared!`] + this type).
276pub struct PolicyHandler<T: Send + Sync + 'static> {
277    cell: &'static crate::InitCell<T>,
278    user: fn(&T, PolicyCtx) -> State,
279}
280
281impl<T: Send + Sync + 'static> PolicyHandler<T> {
282    /// Bind a static state `cell` and a safe `user` callback into a
283    /// handler descriptor usable in `static` declarations.
284    pub const fn new(cell: &'static crate::InitCell<T>, user: fn(&T, PolicyCtx) -> State) -> Self {
285        Self { cell, user }
286    }
287}
288
289// SAFETY: `PolicyHandler<T>` holds only `&'static InitCell<T>` (which is
290// `Sync` when `T: Send + Sync`) and a `fn(&T, PolicyCtx) -> State`
291// pointer; no thread-bound state.  The PM substrate invokes the policy
292// from its own task — the Rust handle is read-only after registration.
293unsafe impl<T: Send + Sync + 'static> Sync for PolicyHandler<T> {}
294
295unsafe extern "C" fn policy_trampoline<T: Send + Sync + 'static>(
296    current: bindings::ove_pm_state_t,
297    idle_ms: u32,
298    next_timeout_ms: u32,
299    user_data: *mut core::ffi::c_void,
300) -> bindings::ove_pm_state_t {
301    if user_data.is_null() {
302        return current;
303    }
304    // SAFETY: `user_data` was set by `set_policy` from a `&'static PolicyHandler<T>`.
305    let h = unsafe { &*(user_data as *const PolicyHandler<T>) };
306    let Some(state) = h.cell.try_get() else {
307        return current;
308    };
309    let ctx = PolicyCtx {
310        current: match current {
311            1 => State::Idle,
312            2 => State::Standby,
313            3 => State::DeepSleep,
314            _ => State::Active,
315        },
316        idle_ms,
317        next_timeout_ms,
318    };
319    (h.user)(state, ctx) as bindings::ove_pm_state_t
320}
321
322/// Register a typed-context power policy handler.
323pub fn set_policy<T: Send + Sync + 'static>(handler: &'static PolicyHandler<T>) -> Result<()> {
324    let rc = unsafe {
325        bindings::ove_pm_set_policy(
326            Some(policy_trampoline::<T>),
327            handler as *const _ as *mut core::ffi::c_void,
328        )
329    };
330    Error::from_code(rc)
331}
332
333/// Restore the default threshold-based power policy.
334pub fn clear_policy() -> Result<()> {
335    let rc = unsafe { bindings::ove_pm_set_policy(None, core::ptr::null_mut()) };
336    Error::from_code(rc)
337}
338
339// ── Notifications ───────────────────────────────────────────────────────
340
341/// Raw-pointer variant of [`notify_register`].
342///
343/// # Safety
344/// The callback and `user_data` must remain valid until unregistered.
345pub unsafe fn notify_register_raw(
346    cb: bindings::ove_pm_notify_fn,
347    user_data: *mut core::ffi::c_void,
348) -> Result<()> {
349    let rc = unsafe { bindings::ove_pm_notify_register(cb, user_data) };
350    Error::from_code(rc)
351}
352
353/// Raw-pointer variant of [`notify_unregister`].
354///
355/// # Safety
356/// Must match a previously registered (cb, user_data) pair.
357pub unsafe fn notify_unregister_raw(
358    cb: bindings::ove_pm_notify_fn,
359    user_data: *mut core::ffi::c_void,
360) -> Result<()> {
361    let rc = unsafe { bindings::ove_pm_notify_unregister(cb, user_data) };
362    Error::from_code(rc)
363}
364
365/// A registered power transition notification handler.
366///
367/// Bound to a static state cell and a safe `fn(&T, Event, State, State)` callback.
368pub struct NotifyHandler<T: Send + Sync + 'static> {
369    cell: &'static crate::InitCell<T>,
370    user: fn(&T, Event, State, State),
371}
372
373impl<T: Send + Sync + 'static> NotifyHandler<T> {
374    /// Bind a static state `cell` and a safe `user` callback into a
375    /// handler descriptor usable in `static` declarations.
376    pub const fn new(cell: &'static crate::InitCell<T>, user: fn(&T, Event, State, State)) -> Self {
377        Self { cell, user }
378    }
379}
380
381// SAFETY: same structure as `PolicyHandler<T>` — a `&'static InitCell<T>`
382// plus a `fn(...)` pointer.  PM dispatches notify callbacks from its
383// own task; the Rust handle is immutable after registration.
384unsafe impl<T: Send + Sync + 'static> Sync for NotifyHandler<T> {}
385
386unsafe extern "C" fn notify_trampoline<T: Send + Sync + 'static>(
387    event: bindings::ove_pm_event_t,
388    from_state: bindings::ove_pm_state_t,
389    to_state: bindings::ove_pm_state_t,
390    user_data: *mut core::ffi::c_void,
391) {
392    if user_data.is_null() {
393        return;
394    }
395    // SAFETY: `user_data` was set by `notify_register` from a `&'static NotifyHandler<T>`.
396    let h = unsafe { &*(user_data as *const NotifyHandler<T>) };
397    let Some(state) = h.cell.try_get() else {
398        return;
399    };
400    let ev = match event {
401        0 => Event::PreSleep,
402        1 => Event::PostWake,
403        _ => return,
404    };
405    let map = |s: bindings::ove_pm_state_t| match s {
406        1 => State::Idle,
407        2 => State::Standby,
408        3 => State::DeepSleep,
409        _ => State::Active,
410    };
411    (h.user)(state, ev, map(from_state), map(to_state));
412}
413
414/// Register a typed-context transition notification handler.
415///
416/// # Errors
417/// Returns [`Error::NoMemory`] if the notifier table is full.
418pub fn notify_register<T: Send + Sync + 'static>(handler: &'static NotifyHandler<T>) -> Result<()> {
419    let rc = unsafe {
420        bindings::ove_pm_notify_register(
421            Some(notify_trampoline::<T>),
422            handler as *const _ as *mut core::ffi::c_void,
423        )
424    };
425    Error::from_code(rc)
426}
427
428/// Unregister a previously registered notification handler.
429///
430/// # Errors
431/// Returns [`Error::NotRegistered`] if the handler was not found.
432pub fn notify_unregister<T: Send + Sync + 'static>(
433    handler: &'static NotifyHandler<T>,
434) -> Result<()> {
435    let rc = unsafe {
436        bindings::ove_pm_notify_unregister(
437            Some(notify_trampoline::<T>),
438            handler as *const _ as *mut core::ffi::c_void,
439        )
440    };
441    Error::from_code(rc)
442}
443
444// ── Statistics ──────────────────────────────────────────────────────────
445
446/// Query accumulated power statistics.
447///
448/// # Errors
449/// Returns [`Error::InvalidParam`] if the PM subsystem is not initialised.
450pub fn get_stats() -> Result<Stats> {
451    let mut raw: bindings::ove_pm_stats = unsafe { core::mem::zeroed() };
452    let rc = unsafe { bindings::ove_pm_get_stats(&mut raw) };
453    Error::from_code(rc)?;
454    Ok(Stats {
455        time_in_state_us: raw.time_in_state_us,
456        transition_count: raw.transition_count,
457        total_runtime_us: raw.total_runtime_us,
458        active_pct_x100: raw.active_pct_x100,
459    })
460}
461
462/// Reset all accumulated power statistics to zero.
463pub fn reset_stats() {
464    unsafe { bindings::ove_pm_reset_stats() }
465}
466
467// ── Power budget ────────────────────────────────────────────────────────
468
469/// Set a target percentage of time in low-power states.
470///
471/// # Errors
472/// Returns [`Error::InvalidParam`] if the PM subsystem is not initialised.
473pub fn set_budget(target_low_power_pct_x100: u32) -> Result<()> {
474    let rc = unsafe { bindings::ove_pm_set_budget(target_low_power_pct_x100) };
475    Error::from_code(rc)
476}
477
478/// Query actual low-power percentage vs. budget target.
479///
480/// Returns the actual low-power percentage in hundredths (0..10000).
481///
482/// # Errors
483/// Returns [`Error::InvalidParam`] if the PM subsystem is not initialised.
484pub fn get_budget_status() -> Result<u32> {
485    let mut actual: u32 = 0;
486    let rc = unsafe { bindings::ove_pm_get_budget_status(&mut actual) };
487    Error::from_code(rc)?;
488    Ok(actual)
489}