Skip to main content

oxigdal_embedded/
power.rs

1//! Power management for embedded systems
2//!
3//! Provides utilities for managing power consumption in resource-constrained
4//! environments.
5//!
6//! # Real power transitions require a BSP-provided controller
7//!
8//! CPU-frequency scaling and peripheral clock/power gating are SoC-vendor
9//! specific (STM32 `RCC`/`PWR`, nRF `CLOCK`/`POWER`, ESP32 RTC/clock, RISC-V
10//! PMU, …) and cannot be expressed portably in a `no_std` crate. This module
11//! therefore does **not** fabricate register writes. Instead it defines the
12//! [`PowerController`] trait as the extension point a board-support package
13//! (BSP) or HAL implements, and [`PowerManager`] delegates every transition to
14//! the installed controller.
15//!
16//! A [`PowerManager`] built with [`PowerManager::new`] holds a
17//! [`NoController`]: it records the requested mode (a software bookkeeping
18//! layer) but performs **no** hardware action for the active modes. Install a
19//! real controller with [`PowerManager::with_controller`] to get actual
20//! transitions, and use [`PowerManager::request_mode_strict`] when a silent
21//! no-op would be a correctness bug.
22
23use crate::error::{EmbeddedError, Result};
24use core::sync::atomic::{AtomicU8, AtomicU16, Ordering};
25
26/// Power mode levels
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
28#[repr(u8)]
29pub enum PowerMode {
30    /// Full performance, maximum power consumption
31    HighPerformance = 0,
32    /// Balanced performance and power
33    Balanced = 1,
34    /// Low power, reduced performance
35    LowPower = 2,
36    /// Ultra low power, minimal functionality
37    UltraLowPower = 3,
38    /// Sleep mode, CPU halted
39    Sleep = 4,
40    /// Deep sleep, most peripherals off
41    DeepSleep = 5,
42}
43
44impl PowerMode {
45    /// Get power mode from u8
46    pub const fn from_u8(value: u8) -> Option<Self> {
47        match value {
48            0 => Some(Self::HighPerformance),
49            1 => Some(Self::Balanced),
50            2 => Some(Self::LowPower),
51            3 => Some(Self::UltraLowPower),
52            4 => Some(Self::Sleep),
53            5 => Some(Self::DeepSleep),
54            _ => None,
55        }
56    }
57
58    /// Get CPU frequency scaling factor (1.0 = full speed)
59    pub const fn cpu_freq_factor(&self) -> f32 {
60        match self {
61            Self::HighPerformance => 1.0,
62            Self::Balanced => 0.75,
63            Self::LowPower => 0.5,
64            Self::UltraLowPower => 0.25,
65            Self::Sleep => 0.0,
66            Self::DeepSleep => 0.0,
67        }
68    }
69
70    /// Check if mode allows CPU execution
71    pub const fn allows_execution(&self) -> bool {
72        !matches!(self, Self::Sleep | Self::DeepSleep)
73    }
74}
75
76/// Board-support extension point for **real** power-state control.
77///
78/// Dynamic CPU-frequency scaling and peripheral clock/power gating are
79/// inherently SoC-vendor-specific: they are driven by device registers such as
80/// the STM32 `RCC`/`PWR` blocks, the nRF `CLOCK`/`POWER` peripherals, the ESP32
81/// RTC/clock subsystem, or a RISC-V PMU. None of that can be expressed portably
82/// at the ARM/RISC-V ISA level, so this crate does **not** fabricate register
83/// pokes. Instead, a board-support package (BSP) or HAL implements this trait
84/// and hands it to [`PowerManager`], which then delegates every transition to
85/// it.
86///
87/// All methods take `&self`: hardware power controllers manipulate
88/// memory-mapped registers (volatile writes to fixed addresses) or hold their
89/// own interior-mutable/atomic state, so shared-reference access is the
90/// idiomatic and `static`-friendly shape for embedded singletons.
91///
92/// # Honesty contract
93///
94/// Implementing this trait is an assertion that [`set_power_mode`] actually
95/// reaches silicon. A controller that cannot perform a transition must return a
96/// typed error (never `Ok(())` for a change it did not make). The only
97/// sanctioned no-op is [`NoController`], which reports
98/// [`is_hardware`] `== false` so the manager and its callers can tell that no
99/// real transition occurred.
100///
101/// [`set_power_mode`]: PowerController::set_power_mode
102/// [`is_hardware`]: PowerController::is_hardware
103pub trait PowerController {
104    /// Apply `mode` to the hardware.
105    ///
106    /// This is the primary hook the BSP must implement. It is expected to
107    /// reprogram clock trees, switch voltage-scaling levels, and gate/ungate
108    /// peripherals as appropriate for the target mode. For the sleep modes the
109    /// manager additionally issues the ISA-level `WFI` after this returns, so an
110    /// implementation only needs to configure wake sources / deep power states
111    /// here.
112    ///
113    /// # Errors
114    ///
115    /// Returns an [`EmbeddedError`] if the transition cannot be performed. An
116    /// implementation must **not** return `Ok(())` for a change it did not
117    /// actually apply.
118    fn set_power_mode(&self, mode: PowerMode) -> Result<()>;
119
120    /// Optional finer-grained hook: apply only the CPU-frequency scaling for
121    /// `mode` (see [`PowerMode::cpu_freq_factor`]).
122    ///
123    /// Provided so a BSP can structure [`set_power_mode`](Self::set_power_mode)
124    /// as `scale_cpu` + `gate_peripherals` if convenient. The default does
125    /// nothing and returns `Ok(())`; overriding it has no effect unless the
126    /// implementation's `set_power_mode` calls it.
127    ///
128    /// # Errors
129    ///
130    /// Returns an [`EmbeddedError`] if the clock reconfiguration fails.
131    fn scale_cpu(&self, mode: PowerMode) -> Result<()> {
132        let _ = mode;
133        Ok(())
134    }
135
136    /// Optional finer-grained hook: gate or ungate peripheral clocks/power for
137    /// `mode`.
138    ///
139    /// See [`scale_cpu`](Self::scale_cpu) for how the default is intended to be
140    /// used.
141    ///
142    /// # Errors
143    ///
144    /// Returns an [`EmbeddedError`] if the peripheral gating fails.
145    fn gate_peripherals(&self, mode: PowerMode) -> Result<()> {
146        let _ = mode;
147        Ok(())
148    }
149
150    /// Whether this controller drives real silicon.
151    ///
152    /// Defaults to `true`. Only host/test stubs — notably
153    /// [`NoController`] — override this to `false` so that
154    /// [`PowerManager::has_hardware_controller`] and
155    /// [`PowerManager::request_mode_strict`] can distinguish a genuine hardware
156    /// backend from a bookkeeping-only placeholder.
157    fn is_hardware(&self) -> bool {
158        true
159    }
160}
161
162/// The default, host/test [`PowerController`] that performs **no** hardware
163/// action.
164///
165/// A [`PowerManager`] built with [`PowerManager::new`] holds a `NoController`.
166/// Its [`set_power_mode`](PowerController::set_power_mode) is an explicit,
167/// documented no-op that returns `Ok(())` — the [`PowerManager`] still records
168/// the requested mode, so the manager behaves as a software bookkeeping layer,
169/// but **no clock tree is reprogrammed and no peripheral is gated**. Because it
170/// reports [`is_hardware`](PowerController::is_hardware) `== false`, callers that
171/// require real transitions can detect its presence via
172/// [`PowerManager::has_hardware_controller`] or fail loudly with
173/// [`PowerManager::request_mode_strict`].
174///
175/// Install a real BSP-provided [`PowerController`] with
176/// [`PowerManager::with_controller`] to get actual power transitions.
177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
178pub struct NoController;
179
180impl PowerController for NoController {
181    /// No-op: records nothing at the hardware level and reports success so the
182    /// owning [`PowerManager`] can act as a bookkeeping layer. See the type
183    /// docs for the honesty contract.
184    fn set_power_mode(&self, mode: PowerMode) -> Result<()> {
185        let _ = mode;
186        Ok(())
187    }
188
189    fn is_hardware(&self) -> bool {
190        false
191    }
192}
193
194/// Power manager for controlling system power state.
195///
196/// # Hardware support (read this)
197///
198/// Real power transitions — dynamic CPU-frequency scaling and peripheral
199/// clock/power gating for `HighPerformance` / `Balanced` / `LowPower` /
200/// `UltraLowPower` / `DeepSleep` — are SoC-vendor-specific and **require a
201/// BSP-provided [`PowerController`]**. This manager never fabricates
202/// vendor-register writes; it delegates every transition to the installed
203/// controller.
204///
205/// The type parameter records which controller is installed:
206///
207/// - [`PowerManager::new`] builds a `PowerManager<`[`NoController`]`>`: a
208///   bookkeeping-only manager. Transitions are validated and the requested mode
209///   is recorded (so [`current_mode`](Self::current_mode) always reflects the
210///   last successful request), but **no hardware is touched** for the four
211///   active modes. This is honest by construction — the type is
212///   `PowerManager<NoController>` and [`has_hardware_controller`] returns
213///   `false`.
214/// - [`PowerManager::with_controller`] builds a `PowerManager<C>` around a real
215///   BSP controller; [`request_mode`](Self::request_mode) then delegates to
216///   [`PowerController::set_power_mode`] and propagates its result.
217///
218/// ## Sleep modes
219///
220/// For [`PowerMode::Sleep`] and [`PowerMode::DeepSleep`] the manager, *after*
221/// delegating to the controller, additionally issues the ISA-level `WFI`
222/// (wait-for-interrupt) on `arm`/`aarch64` and `riscv` targets. `WFI` really
223/// halts the core in a low-power state until a wake event and is target-generic
224/// (not SoC-specific), so it lives here rather than in the controller. On other
225/// targets it is a no-op.
226///
227/// ## Requiring real hardware
228///
229/// Callers that must not proceed without silicon-level control should use
230/// [`request_mode_strict`](Self::request_mode_strict), which returns
231/// [`EmbeddedError::NoPowerController`] when only a [`NoController`] is
232/// installed instead of silently recording a mode that never reached hardware.
233///
234/// [`has_hardware_controller`]: Self::has_hardware_controller
235/// [`current_mode`]: Self::current_mode
236pub struct PowerManager<C: PowerController = NoController> {
237    current_mode: AtomicU8,
238    controller: C,
239}
240
241impl PowerManager<NoController> {
242    /// Create a new bookkeeping-only power manager in high performance mode.
243    ///
244    /// The manager holds a [`NoController`], so transitions are recorded but no
245    /// hardware is reconfigured. Use [`with_controller`](Self::with_controller)
246    /// to install a BSP-provided [`PowerController`] for real transitions.
247    pub const fn new() -> Self {
248        Self {
249            current_mode: AtomicU8::new(PowerMode::HighPerformance as u8),
250            controller: NoController,
251        }
252    }
253}
254
255impl<C: PowerController> PowerManager<C> {
256    /// Create a power manager backed by a BSP-provided [`PowerController`],
257    /// starting in high performance mode.
258    ///
259    /// Every subsequent transition delegates to `controller`.
260    pub const fn with_controller(controller: C) -> Self {
261        Self {
262            current_mode: AtomicU8::new(PowerMode::HighPerformance as u8),
263            controller,
264        }
265    }
266
267    /// Borrow the installed power controller.
268    pub const fn controller(&self) -> &C {
269        &self.controller
270    }
271
272    /// Whether a real hardware [`PowerController`] is installed.
273    ///
274    /// Returns `false` for the default [`NoController`] and `true` for any
275    /// controller that does not override
276    /// [`PowerController::is_hardware`] to `false`.
277    pub fn has_hardware_controller(&self) -> bool {
278        self.controller.is_hardware()
279    }
280
281    /// Get current power mode.
282    pub fn current_mode(&self) -> PowerMode {
283        let mode_u8 = self.current_mode.load(Ordering::Relaxed);
284        PowerMode::from_u8(mode_u8).unwrap_or(PowerMode::HighPerformance)
285    }
286
287    /// Request a power mode transition.
288    ///
289    /// The requested mode is validated and recorded, then applied through the
290    /// installed [`PowerController`]. With the default [`NoController`] the mode
291    /// is recorded but **no hardware is reconfigured** (see the type-level
292    /// docs); with a BSP controller the transition is delegated to
293    /// [`PowerController::set_power_mode`] and its result is propagated. For
294    /// sleep modes the ISA-level `WFI` is issued afterwards on `arm`/`riscv`.
295    ///
296    /// If you must not proceed without real hardware control, use
297    /// [`request_mode_strict`](Self::request_mode_strict) instead.
298    ///
299    /// # Errors
300    ///
301    /// Returns [`EmbeddedError::PowerModeTransitionFailed`] if the transition is
302    /// not allowed, or any error surfaced by the installed controller.
303    pub fn request_mode(&self, mode: PowerMode) -> Result<()> {
304        let current = self.current_mode();
305
306        // Validate transition
307        if !self.is_transition_allowed(current, mode) {
308            return Err(EmbeddedError::PowerModeTransitionFailed);
309        }
310
311        // Record the requested mode before touching hardware so `current_mode`
312        // reflects the intent even if the controller fails partway through.
313        self.current_mode.store(mode as u8, Ordering::Release);
314
315        // Delegate the actual transition to the installed controller.
316        self.apply_mode(mode)
317    }
318
319    /// Request a power mode transition, failing loudly if there is no real
320    /// hardware controller.
321    ///
322    /// Identical to [`request_mode`](Self::request_mode) except that it returns
323    /// [`EmbeddedError::NoPowerController`] — **without** recording the mode or
324    /// touching hardware — when only a bookkeeping [`NoController`] is
325    /// installed. Use this when a silent no-op would be a correctness bug (for
326    /// example, before entering a mode your application depends on for thermal
327    /// or battery limits).
328    ///
329    /// # Errors
330    ///
331    /// Returns [`EmbeddedError::NoPowerController`] if no hardware controller is
332    /// installed, otherwise behaves like
333    /// [`request_mode`](Self::request_mode).
334    pub fn request_mode_strict(&self, mode: PowerMode) -> Result<()> {
335        if !self.controller.is_hardware() {
336            return Err(EmbeddedError::NoPowerController);
337        }
338        self.request_mode(mode)
339    }
340
341    /// Check if a power mode transition is allowed.
342    fn is_transition_allowed(&self, from: PowerMode, to: PowerMode) -> bool {
343        // Allow any transition for now. Real hardware constraints (e.g. illegal
344        // direct jumps between deep states) are the controller's concern and are
345        // surfaced through `PowerController::set_power_mode`.
346        let _ = from;
347        let _ = to;
348        true
349    }
350
351    /// Apply the power mode via the controller, plus the ISA-level `WFI` for
352    /// sleep modes.
353    fn apply_mode(&self, mode: PowerMode) -> Result<()> {
354        // Primary hardware hook: SoC-specific frequency scaling / peripheral
355        // gating / deep-power configuration is the controller's responsibility.
356        self.controller.set_power_mode(mode)?;
357
358        // Sleep modes additionally halt the core at the ISA level. This is
359        // target-generic (not SoC-specific), so it belongs here rather than in
360        // the controller, and it runs after the controller has configured wake
361        // sources / deep-power state.
362        match mode {
363            PowerMode::Sleep | PowerMode::DeepSleep => self.enter_wait_for_interrupt(),
364            _ => Ok(()),
365        }
366    }
367
368    /// Halt the CPU until a wake event using the ISA-level `WFI` instruction.
369    ///
370    /// Real, ISA-level power reduction on bare-metal `arm`/`aarch64`/`riscv`
371    /// targets; a no-op on hosted targets where `WFI` from user space can trap.
372    fn enter_wait_for_interrupt(&self) -> Result<()> {
373        #[cfg(feature = "riscv")]
374        {
375            use crate::target::riscv::power;
376            power::wait_for_interrupt()?;
377        }
378
379        #[cfg(feature = "arm")]
380        {
381            use crate::target::arm::power;
382            power::wait_for_interrupt()?;
383        }
384
385        Ok(())
386    }
387}
388
389impl Default for PowerManager<NoController> {
390    fn default() -> Self {
391        Self::new()
392    }
393}
394
395/// Power consumption estimator
396pub struct PowerEstimator {
397    current_ma: u32,
398    voltage_mv: u32,
399}
400
401impl PowerEstimator {
402    /// Create a new power estimator
403    pub const fn new(voltage_mv: u32) -> Self {
404        Self {
405            current_ma: 0,
406            voltage_mv,
407        }
408    }
409
410    /// Set current consumption in milliamps
411    pub fn set_current(&mut self, current_ma: u32) {
412        self.current_ma = current_ma;
413    }
414
415    /// Get current consumption in milliamps
416    pub const fn current_ma(&self) -> u32 {
417        self.current_ma
418    }
419
420    /// Calculate power consumption in milliwatts
421    pub const fn power_mw(&self) -> u32 {
422        (self.current_ma as u64 * self.voltage_mv as u64 / 1000) as u32
423    }
424
425    /// Estimate battery life in hours
426    pub fn battery_life_hours(&self, battery_mah: u32) -> f32 {
427        if self.current_ma == 0 {
428            return f32::INFINITY;
429        }
430
431        battery_mah as f32 / self.current_ma as f32
432    }
433
434    /// Estimate energy consumption in millijoules per operation
435    pub fn energy_per_op(&self, operation_time_us: u32) -> f32 {
436        let power_w = self.power_mw() as f32 / 1000.0;
437        let time_s = operation_time_us as f32 / 1_000_000.0;
438        power_w * time_s * 1000.0 // Convert to mJ
439    }
440}
441
442/// Wake source for sleep modes
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum WakeSource {
445    /// Timer wake
446    Timer,
447    /// GPIO wake
448    Gpio,
449    /// UART wake
450    Uart,
451    /// Touch sensor wake
452    Touch,
453    /// Any wake source
454    Any,
455}
456
457/// Sleep configuration
458pub struct SleepConfig {
459    /// Wakeup sources
460    pub wake_sources: heapless::Vec<WakeSource, 8>,
461    /// Sleep duration in microseconds (for timer wake)
462    pub duration_us: Option<u64>,
463    /// Enable RTC during sleep
464    pub keep_rtc: bool,
465}
466
467impl Default for SleepConfig {
468    fn default() -> Self {
469        Self {
470            wake_sources: heapless::Vec::new(),
471            duration_us: None,
472            keep_rtc: true,
473        }
474    }
475}
476
477impl SleepConfig {
478    /// Create a new sleep configuration
479    pub const fn new() -> Self {
480        Self {
481            wake_sources: heapless::Vec::new(),
482            duration_us: None,
483            keep_rtc: true,
484        }
485    }
486
487    /// Add a wake source
488    pub fn add_wake_source(&mut self, source: WakeSource) -> Result<()> {
489        self.wake_sources
490            .push(source)
491            .map_err(|_| EmbeddedError::BufferTooSmall {
492                required: 1,
493                available: 0,
494            })
495    }
496
497    /// Set timer wake duration
498    pub fn set_timer_wake(&mut self, duration_us: u64) -> Result<()> {
499        self.duration_us = Some(duration_us);
500        self.add_wake_source(WakeSource::Timer)
501    }
502}
503
504/// Voltage regulator control
505pub struct VoltageRegulator {
506    voltage_mv: AtomicU16, // Actual voltage in millivolts
507}
508
509impl VoltageRegulator {
510    /// Create a new voltage regulator at nominal voltage
511    pub const fn new(nominal_voltage_mv: u16) -> Self {
512        Self {
513            voltage_mv: AtomicU16::new(nominal_voltage_mv),
514        }
515    }
516
517    /// Get current voltage in millivolts
518    pub fn voltage_mv(&self) -> u16 {
519        self.voltage_mv.load(Ordering::Relaxed)
520    }
521
522    /// Set voltage in millivolts
523    ///
524    /// # Errors
525    ///
526    /// Returns error if voltage is out of safe range
527    pub fn set_voltage(&self, voltage_mv: u16) -> Result<()> {
528        // Validate voltage range (0.5V to 5.0V)
529        if !(500..=5000).contains(&voltage_mv) {
530            return Err(EmbeddedError::InvalidParameter);
531        }
532
533        self.voltage_mv.store(voltage_mv, Ordering::Release);
534
535        // Apply voltage change to hardware
536        // This would interface with voltage regulator hardware
537        Ok(())
538    }
539}
540
541#[cfg(test)]
542#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
543mod tests {
544    use super::*;
545    use core::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
546
547    /// A mock hardware controller that records every delegated call so tests can
548    /// assert the manager delegates correctly. Uses atomics for `&self` interior
549    /// mutability, mirroring how a real register-backed controller behaves.
550    struct MockController {
551        calls: AtomicUsize,
552        last_mode: AtomicU8,
553        fail: AtomicBool,
554    }
555
556    impl MockController {
557        const fn new() -> Self {
558            Self {
559                calls: AtomicUsize::new(0),
560                last_mode: AtomicU8::new(0xFF),
561                fail: AtomicBool::new(false),
562            }
563        }
564
565        fn calls(&self) -> usize {
566            self.calls.load(Ordering::Relaxed)
567        }
568
569        fn last_mode(&self) -> Option<PowerMode> {
570            PowerMode::from_u8(self.last_mode.load(Ordering::Relaxed))
571        }
572    }
573
574    impl PowerController for MockController {
575        fn set_power_mode(&self, mode: PowerMode) -> Result<()> {
576            self.calls.fetch_add(1, Ordering::Relaxed);
577            self.last_mode.store(mode as u8, Ordering::Relaxed);
578            if self.fail.load(Ordering::Relaxed) {
579                return Err(EmbeddedError::HardwareError);
580            }
581            Ok(())
582        }
583    }
584
585    #[test]
586    fn test_default_manager_has_no_hardware_controller() {
587        let pm = PowerManager::new();
588        assert!(!pm.has_hardware_controller());
589        // Bookkeeping still works: the mode is recorded even without hardware.
590        pm.request_mode(PowerMode::LowPower)
591            .expect("bookkeeping transition should succeed");
592        assert_eq!(pm.current_mode(), PowerMode::LowPower);
593    }
594
595    #[test]
596    fn test_no_controller_strict_fails_loud() {
597        let pm = PowerManager::new();
598        // Without a hardware controller the strict API must fail loudly and must
599        // NOT record the requested mode.
600        let err = pm
601            .request_mode_strict(PowerMode::UltraLowPower)
602            .expect_err("strict transition without controller must fail");
603        assert_eq!(err, EmbeddedError::NoPowerController);
604        assert_eq!(pm.current_mode(), PowerMode::HighPerformance);
605    }
606
607    #[test]
608    fn test_noop_controller_is_explicit() {
609        let controller = NoController;
610        assert!(!controller.is_hardware());
611        assert!(controller.set_power_mode(PowerMode::Balanced).is_ok());
612    }
613
614    #[test]
615    fn test_manager_delegates_to_controller() {
616        let pm = PowerManager::with_controller(MockController::new());
617        assert!(pm.has_hardware_controller());
618
619        pm.request_mode(PowerMode::Balanced)
620            .expect("delegated transition should succeed");
621        assert_eq!(pm.current_mode(), PowerMode::Balanced);
622        assert_eq!(pm.controller().calls(), 1);
623        assert_eq!(pm.controller().last_mode(), Some(PowerMode::Balanced));
624
625        pm.request_mode(PowerMode::UltraLowPower)
626            .expect("second transition should succeed");
627        assert_eq!(pm.controller().calls(), 2);
628        assert_eq!(pm.controller().last_mode(), Some(PowerMode::UltraLowPower));
629    }
630
631    #[test]
632    fn test_manager_delegates_sleep_modes() {
633        let pm = PowerManager::with_controller(MockController::new());
634        // Sleep/DeepSleep also delegate to the controller (which configures wake
635        // sources / deep-power state) before the ISA-level WFI.
636        pm.request_mode(PowerMode::Sleep)
637            .expect("sleep transition should succeed");
638        assert_eq!(pm.controller().last_mode(), Some(PowerMode::Sleep));
639
640        pm.request_mode(PowerMode::DeepSleep)
641            .expect("deep sleep transition should succeed");
642        assert_eq!(pm.controller().last_mode(), Some(PowerMode::DeepSleep));
643        assert_eq!(pm.controller().calls(), 2);
644    }
645
646    #[test]
647    fn test_manager_propagates_controller_error() {
648        let pm = PowerManager::with_controller(MockController::new());
649        pm.controller().fail.store(true, Ordering::Relaxed);
650
651        let err = pm
652            .request_mode(PowerMode::LowPower)
653            .expect_err("controller failure must propagate");
654        assert_eq!(err, EmbeddedError::HardwareError);
655        // The controller was still invoked.
656        assert_eq!(pm.controller().calls(), 1);
657    }
658
659    #[test]
660    fn test_strict_delegates_with_hardware_controller() {
661        let pm = PowerManager::with_controller(MockController::new());
662        pm.request_mode_strict(PowerMode::LowPower)
663            .expect("strict transition with controller should succeed");
664        assert_eq!(pm.current_mode(), PowerMode::LowPower);
665        assert_eq!(pm.controller().calls(), 1);
666    }
667
668    #[test]
669    fn test_power_mode_ordering() {
670        assert!(PowerMode::HighPerformance < PowerMode::LowPower);
671        assert!(PowerMode::LowPower < PowerMode::Sleep);
672    }
673
674    #[test]
675    fn test_power_manager() {
676        let pm = PowerManager::new();
677        assert_eq!(pm.current_mode(), PowerMode::HighPerformance);
678
679        pm.request_mode(PowerMode::LowPower)
680            .expect("mode change failed");
681        assert_eq!(pm.current_mode(), PowerMode::LowPower);
682    }
683
684    #[test]
685    fn test_power_manager_sleep_modes() {
686        // Sleep / DeepSleep transitions must succeed and update the tracked
687        // mode. Under the arm/riscv features these route through the ISA-level
688        // WFI hooks (host stubs return Ok), exercising the wiring.
689        let pm = PowerManager::new();
690
691        pm.request_mode(PowerMode::Sleep)
692            .expect("sleep transition failed");
693        assert_eq!(pm.current_mode(), PowerMode::Sleep);
694
695        pm.request_mode(PowerMode::DeepSleep)
696            .expect("deep sleep transition failed");
697        assert_eq!(pm.current_mode(), PowerMode::DeepSleep);
698
699        pm.request_mode(PowerMode::HighPerformance)
700            .expect("wake transition failed");
701        assert_eq!(pm.current_mode(), PowerMode::HighPerformance);
702    }
703
704    #[test]
705    fn test_power_estimator() {
706        let mut estimator = PowerEstimator::new(3300); // 3.3V
707        estimator.set_current(100); // 100mA
708
709        assert_eq!(estimator.power_mw(), 330); // 330mW
710
711        let battery_life = estimator.battery_life_hours(1000); // 1000mAh battery
712        assert_eq!(battery_life, 10.0); // 10 hours
713    }
714
715    #[test]
716    fn test_sleep_config() {
717        let mut config = SleepConfig::new();
718        config
719            .add_wake_source(WakeSource::Gpio)
720            .expect("add failed");
721        config.set_timer_wake(1_000_000).expect("set timer failed");
722
723        assert_eq!(config.wake_sources.len(), 2);
724    }
725
726    #[test]
727    fn test_voltage_regulator() {
728        let regulator = VoltageRegulator::new(3300);
729        assert_eq!(regulator.voltage_mv(), 3300);
730
731        regulator.set_voltage(1800).expect("voltage change failed");
732        assert_eq!(regulator.voltage_mv(), 1800);
733
734        // Test invalid voltage
735        assert!(regulator.set_voltage(6000).is_err());
736    }
737}