Skip to main content

m5stack_core/io/
rpm.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2use embassy_time::{Duration, Ticker};
3use esp_hal::gpio::{AnyPin, Input, InputConfig};
4
5use crate::driver::pcnt::PcntDriver;
6
7pub struct RpmConfig {
8    pub loop_time_ms: u64,
9    pub pole_pairs: f32,
10    pub pulley_ratio: f32,
11}
12
13pub struct RpmResources<'a> {
14    pub pcnt: esp_hal::peripherals::PCNT<'a>,
15    pub pin: AnyPin<'a>,
16}
17
18impl RpmResources<'static> {
19    pub fn into_driver(self) -> PcntDriver {
20        let input = Input::new(
21            self.pin,
22            InputConfig::default().with_pull(esp_hal::gpio::Pull::Down),
23        );
24        PcntDriver::new(self.pcnt, input)
25    }
26}
27
28/// Single-shot RPM read. Calls pcnt.get_and_reset(), applies config.
29pub fn read_rpm(pcnt: &mut PcntDriver, config: &RpmConfig) -> f32 {
30    let pulse_count = pcnt.get_and_reset();
31    pulse_count as f32
32        * 60.                                  // Hz -> rpm
33        * (1. / config.pole_pairs / 2.)        // pole pairs, 2 imp per rev
34        * (1000. / config.loop_time_ms as f32) // intervals per second
35        * config.pulley_ratio
36}
37
38/// Number of consecutive zero-pulse intervals before we report "no signal"
39/// (NaN) instead of "engine stopped" (0.0). At the default 100 ms interval
40/// this is 3 s.
41const NO_SIGNAL_INTERVALS: u32 = 30;
42
43/// Convenience loop: ticker + read_rpm + callback.
44///
45/// `on_rpm` is invoked only on **state transitions**, so callers that
46/// store into a shared atomic don't get hammered every 100 ms:
47///   * each non-zero reading       → `rpm` (measured value)
48///   * first zero after non-zero   → `0.0` (engine just stopped)
49///   * [`NO_SIGNAL_INTERVALS`] zeros → `f32::NAN` (signal lost)
50///
51/// Between the "stopped" and "no signal" transitions the callback is
52/// silent, which lets HIL-injected values stick once the rig has been
53/// idle long enough.
54pub async fn rpm_loop(resources: RpmResources<'static>, config: RpmConfig, on_rpm: fn(f32)) {
55    let mut pcnt_driver = resources.into_driver();
56    let mut ticker = Ticker::every(Duration::from_millis(config.loop_time_ms));
57    let mut zero_intervals: u32 = 0;
58    loop {
59        let rpm = read_rpm(&mut pcnt_driver, &config);
60        if rpm > 0.0 {
61            zero_intervals = 0;
62            on_rpm(rpm);
63        } else {
64            let prev = zero_intervals;
65            zero_intervals = zero_intervals.saturating_add(1);
66            if prev == 0 {
67                on_rpm(0.0);                // transition: running → stopped
68            } else if prev == NO_SIGNAL_INTERVALS {
69                on_rpm(f32::NAN);            // transition: stopped → no signal
70            }
71            // else: PROCESS_DATA untouched
72        }
73        ticker.next().await;
74    }
75}