Added stm32 counter abstraction

This commit is contained in:
Zachary Levy
2026-04-08 19:15:00 -07:00
parent fef05b937d
commit b3906b08e4
3 changed files with 173 additions and 1 deletions
+1
View File
@@ -11,6 +11,7 @@ license.workspace = true
comms = [] comms = []
single-packet-msgs = [] single-packet-msgs = []
usb = ["embassy-usb"] usb = ["embassy-usb"]
counter = []
stm32 = ["embassy-stm32", "physical/stm32"] stm32 = ["embassy-stm32", "physical/stm32"]
[dependencies.physical] [dependencies.physical]
+169
View File
@@ -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<T>>,
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<T, Ch1>>,
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<T, Ch2>>,
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()
}
}
+2
View File
@@ -1,2 +1,4 @@
#[cfg(feature = "usb")] #[cfg(feature = "usb")]
pub mod usb; pub mod usb;
#[cfg(feature = "counter")]
pub mod counter;