diff --git a/embassy-stm32/src/timer/input_capture.rs b/embassy-stm32/src/timer/input_capture.rs
index 000938a70..b3434ae63 100644
--- a/embassy-stm32/src/timer/input_capture.rs
+++ b/embassy-stm32/src/timer/input_capture.rs
@@ -80,18 +80,18 @@ impl<'d, T: GeneralInstance4Channel> InputCapture<'d, T> {
     }
 
     fn new_inner(tim: impl Peripheral<P = T> + 'd, freq: Hertz, counting_mode: CountingMode) -> Self {
-        let mut this = Self { inner: Timer::new(tim) };
+        let mut inner = Timer::new(tim);
 
-        this.inner.set_counting_mode(counting_mode);
-        this.set_tick_freq(freq);
-        this.inner.enable_outputs(); // Required for advanced timers, see GeneralInstance4Channel for details
-        this.inner.start();
+        inner.set_counting_mode(counting_mode);
+        inner.set_tick_freq(freq);
+        inner.enable_outputs(); // Required for advanced timers, see GeneralInstance4Channel for details
+        inner.start();
 
         // enable NVIC interrupt
         T::CaptureCompareInterrupt::unpend();
         unsafe { T::CaptureCompareInterrupt::enable() };
 
-        this
+        Self { inner }
     }
 
     /// Enable the given channel.
@@ -109,24 +109,6 @@ impl<'d, T: GeneralInstance4Channel> InputCapture<'d, T> {
         self.inner.get_channel_enable_state(channel)
     }
 
-    /// Set tick frequency.
-    ///
-    /// Note: when you call this, the max period value changes
-    pub fn set_tick_freq(&mut self, freq: Hertz) {
-        let f = freq;
-        assert!(f.0 > 0);
-        let timer_f = self.inner.get_clock_frequency();
-
-        let pclk_ticks_per_timer_period = timer_f / f;
-        let psc: u16 = unwrap!((pclk_ticks_per_timer_period - 1).try_into());
-
-        let regs = self.inner.regs_core();
-        regs.psc().write_value(psc);
-
-        // Generate an Update Request
-        regs.egr().write(|r| r.set_ug(true));
-    }
-
     /// Set the input capture mode for a given channel.
     pub fn set_input_capture_mode(&mut self, channel: Channel, mode: InputCaptureMode) {
         self.inner.set_input_capture_mode(channel, mode);
@@ -150,7 +132,8 @@ impl<'d, T: GeneralInstance4Channel> InputCapture<'d, T> {
     fn new_future(&self, channel: Channel, mode: InputCaptureMode, tisel: InputTISelection) -> InputCaptureFuture<T> {
         use stm32_metapac::timer::vals::FilterValue;
 
-        // Configuration steps from ST RM0390 chapter 17.3.5 Input Capture Mode
+        // Configuration steps from ST RM0390 (STM32F446) chapter 17.3.5
+        // or ST RM0008 (STM32F103) chapter 15.3.5 Input capture mode
         self.inner.set_input_ti_selection(channel, tisel);
         self.inner.set_input_capture_filter(channel, FilterValue::NOFILTER);
         self.inner.set_input_capture_mode(channel, mode);
diff --git a/embassy-stm32/src/timer/low_level.rs b/embassy-stm32/src/timer/low_level.rs
index 7f533b75c..141e96894 100644
--- a/embassy-stm32/src/timer/low_level.rs
+++ b/embassy-stm32/src/timer/low_level.rs
@@ -273,6 +273,22 @@ impl<'d, T: CoreInstance> Timer<'d, T> {
         }
     }
 
+    /// Set tick frequency.
+    pub fn set_tick_freq(&mut self, freq: Hertz) {
+        let f = freq;
+        assert!(f.0 > 0);
+        let timer_f = self.get_clock_frequency();
+
+        let pclk_ticks_per_timer_period = timer_f / f;
+        let psc: u16 = unwrap!((pclk_ticks_per_timer_period - 1).try_into());
+
+        let regs = self.regs_core();
+        regs.psc().write_value(psc);
+
+        // Generate an Update Request
+        regs.egr().write(|r| r.set_ug(true));
+    }
+
     /// Clear update interrupt.
     ///
     /// Returns whether the update interrupt flag was set.
diff --git a/embassy-stm32/src/timer/mod.rs b/embassy-stm32/src/timer/mod.rs
index 314b6006b..25782ee13 100644
--- a/embassy-stm32/src/timer/mod.rs
+++ b/embassy-stm32/src/timer/mod.rs
@@ -8,6 +8,7 @@ use embassy_sync::waitqueue::AtomicWaker;
 pub mod complementary_pwm;
 pub mod input_capture;
 pub mod low_level;
+pub mod pwm_input;
 pub mod qei;
 pub mod simple_pwm;
 
diff --git a/embassy-stm32/src/timer/pwm_input.rs b/embassy-stm32/src/timer/pwm_input.rs
new file mode 100644
index 000000000..d34ba086f
--- /dev/null
+++ b/embassy-stm32/src/timer/pwm_input.rs
@@ -0,0 +1,134 @@
+//! Input capture driver.
+
+use embassy_hal_internal::into_ref;
+
+use super::low_level::{CountingMode, InputCaptureMode, InputTISelection, Timer};
+use super::{Channel, Channel1Pin, Channel2Pin, GeneralInstance4Channel};
+use crate::gpio::{AFType, Pull};
+use crate::time::Hertz;
+use crate::Peripheral;
+
+/// Input capture driver.
+pub struct PwmInput<'d, T: GeneralInstance4Channel> {
+    channel: Channel,
+    inner: Timer<'d, T>,
+}
+
+/// Convert pointer to TIM instance to TimGp16 object
+fn regs_gp16(ptr: *mut ()) -> crate::pac::timer::TimGp16 {
+    unsafe { crate::pac::timer::TimGp16::from_ptr(ptr) }
+}
+
+impl<'d, T: GeneralInstance4Channel> PwmInput<'d, T> {
+    /// Create a new input capture driver.
+    pub fn new(
+        tim: impl Peripheral<P = T> + 'd,
+        pin: impl Peripheral<P = impl Channel1Pin<T>> + 'd,
+        pull_type: Pull,
+        freq: Hertz,
+    ) -> Self {
+        into_ref!(pin);
+        critical_section::with(|_| {
+            pin.set_as_af_pull(pin.af_num(), AFType::Input, pull_type);
+            #[cfg(gpio_v2)]
+            pin.set_speed(crate::gpio::Speed::VeryHigh);
+        });
+
+        Self::new_inner(tim, freq, Channel::Ch1, Channel::Ch2)
+    }
+
+    /// Create a new input capture driver.
+    pub fn new_alt(
+        tim: impl Peripheral<P = T> + 'd,
+        pin: impl Peripheral<P = impl Channel2Pin<T>> + 'd,
+        pull_type: Pull,
+        freq: Hertz,
+    ) -> Self {
+        into_ref!(pin);
+        critical_section::with(|_| {
+            pin.set_as_af_pull(pin.af_num(), AFType::Input, pull_type);
+            #[cfg(gpio_v2)]
+            pin.set_speed(crate::gpio::Speed::VeryHigh);
+        });
+
+        Self::new_inner(tim, freq, Channel::Ch2, Channel::Ch1)
+    }
+
+    fn new_inner(tim: impl Peripheral<P = T> + 'd, freq: Hertz, ch1: Channel, ch2: Channel) -> Self {
+        use stm32_metapac::timer::vals::{Sms, Ts};
+
+        let mut inner = Timer::new(tim);
+
+        inner.set_counting_mode(CountingMode::EdgeAlignedUp);
+        inner.set_tick_freq(freq);
+        inner.enable_outputs(); // Required for advanced timers, see GeneralInstance4Channel for details
+        inner.start();
+
+        // Configuration steps from ST RM0390 (STM32F446) chapter 17.3.6
+        // or ST RM0008 (STM32F103) chapter 15.3.6 Input capture mode
+        inner.set_input_ti_selection(ch1, InputTISelection::Normal);
+        inner.set_input_capture_mode(ch1, InputCaptureMode::Rising);
+
+        inner.set_input_ti_selection(ch2, InputTISelection::Alternate);
+        inner.set_input_capture_mode(ch2, InputCaptureMode::Falling);
+
+        let regs = regs_gp16(T::regs());
+        regs.smcr().modify(|r| {
+            // Select the valid trigger input: write the TS bits to 101 in the TIMx_SMCR register
+            // (TI1FP1 selected).
+            r.set_ts(match ch1 {
+                Channel::Ch1 => Ts::TI1FP1,
+                Channel::Ch2 => Ts::TI2FP2,
+                _ => panic!("Invalid channel for PWM input"),
+            });
+
+            // Configure the slave mode controller in reset mode: write the SMS bits to 100 in the
+            // TIMx_SMCR register.
+            r.set_sms(Sms::RESET_MODE);
+        });
+
+        // Must call the `enable` function after
+
+        Self { channel: ch1, inner }
+    }
+
+    /// Enable the given channel.
+    pub fn enable(&mut self) {
+        self.inner.enable_channel(Channel::Ch1, true);
+        self.inner.enable_channel(Channel::Ch2, true);
+    }
+
+    /// Disable the given channel.
+    pub fn disable(&mut self) {
+        self.inner.enable_channel(Channel::Ch1, false);
+        self.inner.enable_channel(Channel::Ch2, false);
+    }
+
+    /// Check whether given channel is enabled
+    pub fn is_enabled(&self) -> bool {
+        self.inner.get_channel_enable_state(Channel::Ch1)
+    }
+
+    /// Get the period tick count
+    pub fn get_period_ticks(&self) -> u32 {
+        self.inner.get_capture_value(self.channel)
+    }
+
+    /// Get the duty tick count
+    pub fn get_duty_ticks(&self) -> u32 {
+        self.inner.get_capture_value(match self.channel {
+            Channel::Ch1 => Channel::Ch2,
+            Channel::Ch2 => Channel::Ch1,
+            _ => panic!("Invalid channel for PWM input"),
+        })
+    }
+
+    /// Get the duty cycle in 100%
+    pub fn get_duty_cycle(&self) -> f32 {
+        let period = self.get_period_ticks();
+        if period == 0 {
+            return 0.;
+        }
+        100. * (self.get_duty_ticks() as f32) / (period as f32)
+    }
+}
diff --git a/examples/stm32f1/.vscode/launch.json b/examples/stm32f1/.vscode/launch.json
index 7d1504a39..998508867 100644
--- a/examples/stm32f1/.vscode/launch.json
+++ b/examples/stm32f1/.vscode/launch.json
@@ -15,7 +15,7 @@
             "cwd": "${workspaceRoot}",
             "preLaunchTask": "Cargo Build (debug)",
             "runToEntryPoint": "main",
-            "executable": "./target/thumbv7m-none-eabi/debug/input_capture",
+            "executable": "./target/thumbv7m-none-eabi/debug/pwm_input",
             /* Run `cargo build --example itm` and uncomment this line to run itm example */
             // "executable": "./target/thumbv7em-none-eabihf/debug/examples/itm",
             "device": "STM32F103TB",
diff --git a/examples/stm32f1/.vscode/tasks.json b/examples/stm32f1/.vscode/tasks.json
index e153722da..de7013b12 100644
--- a/examples/stm32f1/.vscode/tasks.json
+++ b/examples/stm32f1/.vscode/tasks.json
@@ -9,7 +9,7 @@
 			],
 			"args": [
 				"--bin",
-				"input_capture"
+				"pwm_input"
 			],
 			"group": {
 				"kind": "build",
diff --git a/examples/stm32f1/src/bin/pwm_input.rs b/examples/stm32f1/src/bin/pwm_input.rs
new file mode 100644
index 000000000..14978f817
--- /dev/null
+++ b/examples/stm32f1/src/bin/pwm_input.rs
@@ -0,0 +1,50 @@
+#![no_std]
+#![no_main]
+
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_stm32::gpio::{Level, Output, Pull, Speed};
+use embassy_stm32::time::khz;
+use embassy_stm32::timer::{self, pwm_input::PwmInput};
+use embassy_stm32::{bind_interrupts, peripherals};
+use embassy_time::Timer;
+use {defmt_rtt as _, panic_probe as _};
+
+/// Connect PB2 and PB10 with a 1k Ohm resistor
+
+#[embassy_executor::task]
+async fn blinky(led: peripherals::PC13) {
+    let mut led = Output::new(led, Level::High, Speed::Low);
+
+    loop {
+        info!("high");
+        led.set_high();
+        Timer::after_millis(300).await;
+
+        info!("low");
+        led.set_low();
+        Timer::after_millis(300).await;
+    }
+}
+
+bind_interrupts!(struct Irqs {
+    TIM2 => timer::CaptureCompareInterruptHandler<peripherals::TIM2>;
+});
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+    let p = embassy_stm32::init(Default::default());
+    info!("Hello World!");
+
+    unwrap!(spawner.spawn(blinky(p.PC13)));
+
+    let pwm_input = PwmInput::new(p.TIM2, p.PA0, Pull::None, khz(1000));
+
+    loop {
+        Timer::after_millis(500).await;
+        let _per = pwm_input.get_period_ticks();
+        let _dc = pwm_input.get_duty_ticks();
+        let _pc = pwm_input.get_duty_cycle();
+        asm::nop();
+    }
+}
diff --git a/examples/stm32f4/.vscode/launch.json b/examples/stm32f4/.vscode/launch.json
index 20cd4d2e8..a9849e0da 100644
--- a/examples/stm32f4/.vscode/launch.json
+++ b/examples/stm32f4/.vscode/launch.json
@@ -15,7 +15,7 @@
             "cwd": "${workspaceRoot}",
             "preLaunchTask": "Cargo Build (debug)",
             "runToEntryPoint": "main",
-            "executable": "./target/thumbv7em-none-eabihf/debug/input_capture",
+            "executable": "./target/thumbv7em-none-eabihf/debug/pwm_input",
             /* Run `cargo build --example itm` and uncomment this line to run itm example */
             // "executable": "./target/thumbv7em-none-eabihf/debug/examples/itm",
             "device": "STM32F446RET6",
diff --git a/examples/stm32f4/.vscode/tasks.json b/examples/stm32f4/.vscode/tasks.json
index e153722da..de7013b12 100644
--- a/examples/stm32f4/.vscode/tasks.json
+++ b/examples/stm32f4/.vscode/tasks.json
@@ -9,7 +9,7 @@
 			],
 			"args": [
 				"--bin",
-				"input_capture"
+				"pwm_input"
 			],
 			"group": {
 				"kind": "build",
diff --git a/examples/stm32f4/src/bin/pwm_input.rs b/examples/stm32f4/src/bin/pwm_input.rs
new file mode 100644
index 000000000..e57e58c22
--- /dev/null
+++ b/examples/stm32f4/src/bin/pwm_input.rs
@@ -0,0 +1,52 @@
+#![no_std]
+#![no_main]
+
+use cortex_m::asm;
+use defmt::*;
+use embassy_executor::Spawner;
+use embassy_stm32::gpio::{Level, Output, Pull, Speed};
+use embassy_stm32::time::khz;
+use embassy_stm32::timer::{self, pwm_input::PwmInput};
+use embassy_stm32::{bind_interrupts, peripherals};
+use embassy_time::Timer;
+use {defmt_rtt as _, panic_probe as _};
+
+/// Connect PB2 and PB10 with a 1k Ohm resistor
+
+#[embassy_executor::task]
+async fn blinky(led: peripherals::PB2) {
+    let mut led = Output::new(led, Level::High, Speed::Low);
+
+    loop {
+        info!("high");
+        led.set_high();
+        Timer::after_millis(300).await;
+
+        info!("low");
+        led.set_low();
+        Timer::after_millis(300).await;
+    }
+}
+
+bind_interrupts!(struct Irqs {
+    TIM2 => timer::CaptureCompareInterruptHandler<peripherals::TIM2>;
+});
+
+#[embassy_executor::main]
+async fn main(spawner: Spawner) {
+    let p = embassy_stm32::init(Default::default());
+    info!("Hello World!");
+
+    unwrap!(spawner.spawn(blinky(p.PB2)));
+
+    let mut pwm_input = PwmInput::new(p.TIM3, p.PA6, Pull::None, khz(10));
+    pwm_input.enable();
+
+    loop {
+        Timer::after_millis(500).await;
+        let _per = pwm_input.get_period_ticks();
+        let _dc = pwm_input.get_duty_ticks();
+        let _pc = pwm_input.get_duty_cycle();
+        asm::nop();
+    }
+}