diff --git a/node/Cargo.toml b/node/Cargo.toml index b9904e7..7244042 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true comms = [] single-packet-msgs = [] usb = ["embassy-usb"] +counter = [] stm32 = ["embassy-stm32", "physical/stm32"] [dependencies.physical] diff --git a/node/src/stm32/counter.rs b/node/src/stm32/counter.rs new file mode 100644 index 0000000..705211d --- /dev/null +++ b/node/src/stm32/counter.rs @@ -0,0 +1,169 @@ +//! Hardware pulse counter using a timer in External Clock Mode. +//! +//! Counts edges on an external signal entirely in hardware. Supports three +//! pin sources per timer: the dedicated ETR pin, or any CH1/CH2 pin. + +use embassy_stm32::gpio::{AfType, Flex, Pull}; +use embassy_stm32::pac::timer::vals::{Etp, Etps}; +use embassy_stm32::timer::low_level::{ + FilterValue, InputCaptureMode, InputTISelection, SlaveMode, Timer, TriggerSource, +}; +use embassy_stm32::timer::{ + Ch1, Ch2, Channel, ExternalTriggerPin, GeneralInstance4Channel, TimerPin, +}; +use embassy_stm32::Peri; + +/// Which edge increments the counter. +#[derive(Clone, Copy, Default, defmt::Format)] +pub enum CountEdge { + #[default] + Rising, + Falling, + /// Count on both rising and falling edges. + /// Only supported with channel pins (CH1/CH2), not ETR. + Both, +} + +/// Pulse counter configuration. +#[derive(Clone)] +pub struct PulseCounterConfig { + pub edge: CountEdge, + pub filter: FilterValue, + pub pull: Pull, +} + +impl Default for PulseCounterConfig { + fn default() -> Self { + Self { + edge: CountEdge::Rising, + filter: FilterValue::NO_FILTER, + pull: Pull::None, + } + } +} + +/// Hardware pulse counter driven by an external signal. +/// +/// Takes ownership of the timer and pin, preventing reuse elsewhere. +pub struct PulseCounter<'d, T: GeneralInstance4Channel> { + inner: Timer<'d, T>, + _pin: Flex<'d>, +} + +// ---- Constructors ---- + +impl<'d, T: GeneralInstance4Channel> PulseCounter<'d, T> { + /// Count pulses on the timer's ETR (External Trigger) pin. + /// + /// # Panics + /// Panics if `config.edge` is [`CountEdge::Both`] — ETR only supports single-edge detection. + pub fn new_etr( + tim: Peri<'d, T>, + pin: Peri<'d, impl ExternalTriggerPin>, + config: PulseCounterConfig, + ) -> Self { + assert!( + !matches!(config.edge, CountEdge::Both), + "ETR does not support both-edge detection" + ); + + let af_num = pin.af_num(); + let mut flex_pin = Flex::new(pin); + flex_pin.set_as_af_unchecked(af_num, AfType::input(config.pull)); + + let inner = Timer::new(tim); + + inner.set_slave_mode(SlaveMode::EXT_CLOCK_MODE); + inner.set_trigger_source(TriggerSource::ETRF); + inner.set_external_trigger_filter(config.filter); + inner.set_external_trigger_prescaler(Etps::DIV1); + inner.set_external_trigger_polarity(match config.edge { + CountEdge::Rising => Etp::from(0), + CountEdge::Falling => Etp::from(1), + CountEdge::Both => unreachable!(), + }); + + inner.start(); + Self { inner, _pin: flex_pin } + } + + /// Count pulses on a CH1 pin. + pub fn new_ch1( + tim: Peri<'d, T>, + pin: Peri<'d, impl TimerPin>, + config: PulseCounterConfig, + ) -> Self { + let af_num = pin.af_num(); + let mut flex_pin = Flex::new(pin); + flex_pin.set_as_af_unchecked(af_num, AfType::input(config.pull)); + + let inner = Timer::new(tim); + Self::configure_channel(&inner, Channel::Ch1, TriggerSource::TI1FP1, &config); + + inner.start(); + Self { inner, _pin: flex_pin } + } + + /// Count pulses on a CH2 pin. + pub fn new_ch2( + tim: Peri<'d, T>, + pin: Peri<'d, impl TimerPin>, + config: PulseCounterConfig, + ) -> Self { + let af_num = pin.af_num(); + let mut flex_pin = Flex::new(pin); + flex_pin.set_as_af_unchecked(af_num, AfType::input(config.pull)); + + let inner = Timer::new(tim); + Self::configure_channel(&inner, Channel::Ch2, TriggerSource::TI2FP2, &config); + + inner.start(); + Self { inner, _pin: flex_pin } + } + + /// Common channel setup: put the channel in input mode, configure its + /// filter and edge polarity, then wire it as the slave-mode trigger. + fn configure_channel( + inner: &Timer<'d, T>, + channel: Channel, + trigger: TriggerSource, + config: &PulseCounterConfig, + ) { + // Channel must be in input mode for the ICxF filter bits to exist. + // Normal = direct mapping (TI1→IC1, TI2→IC2). + inner.set_input_ti_selection(channel, InputTISelection::Normal); + inner.set_input_capture_filter(channel, config.filter); + inner.set_input_capture_mode(channel, match config.edge { + CountEdge::Rising => InputCaptureMode::Rising, + CountEdge::Falling => InputCaptureMode::Falling, + CountEdge::Both => InputCaptureMode::BothEdges, + }); + + inner.set_trigger_source(trigger); + inner.set_slave_mode(SlaveMode::EXT_CLOCK_MODE); + } +} + +// ---- Reading ---- + +impl<'d, T: GeneralInstance4Channel> PulseCounter<'d, T> { + /// Read the current count (lower 16 bits). + #[inline] + pub fn count(&self) -> u16 { + self.inner.regs_gp16().cnt().read().cnt() + } + + /// Reset the counter to zero. + #[inline] + pub fn reset(&self) { + self.inner.reset(); + } +} + +impl<'d, T: embassy_stm32::timer::GeneralInstance32bit4Channel> PulseCounter<'d, T> { + /// Read the current count as a full 32-bit value. + #[inline] + pub fn count_32(&self) -> u32 { + self.inner.regs_gp32().cnt().read() + } +} diff --git a/node/src/stm32/mod.rs b/node/src/stm32/mod.rs index 5ff0b93..f3312ef 100644 --- a/node/src/stm32/mod.rs +++ b/node/src/stm32/mod.rs @@ -1,2 +1,4 @@ #[cfg(feature = "usb")] -pub mod usb; \ No newline at end of file +pub mod usb; +#[cfg(feature = "counter")] +pub mod counter;