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}