Skip to content

Conversation

azerupi
Copy link

@azerupi azerupi commented Jul 25, 2025

This adds support for capturing a sequence using input capture and DMA. This allows to decode e.g. PWM signals or more interestingly a signal with non-constant duty cycle e.g. DShot.

This is in a way the inverse behavior of waveform_up. On each edge transition the timestamp of the timer is stored in the provided buffer using the DMA until the buffer is full.

It seems to work, but I am not familiar with the embassy code and I would like to have eyes on this to see if what I'm doing anything seems correct. There are also some improvements I would like to do for which support would be appreciated.

  1. Is there a way we can remove the need to provide the timer channel as a generic type parameter to make it slightly more ergonomic for the user?
  2. Is there anything special to do to support 32-bit timers? I believe right now the word size is inferred from the buffer type but I don't know if that is ok.
  3. I would like to have this method or an abstraction on top to compute the duration between the timestamps automatically instead of having the user to do this by using the tick frequency that was provided to the timer. What do you think about this?

An example of usage is given in the code below.

#![no_std]
#![no_main]

use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, OutputType, Pull, Speed};
use embassy_stm32::time::{khz, mhz};
use embassy_stm32::timer::input_capture::{CapturePin, InputCapture};
use embassy_stm32::timer::low_level::{FilterValue, InputCaptureMode};
use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm};
use embassy_stm32::timer::{Ch1, Channel};
use embassy_stm32::{bind_interrupts, peripherals, timer, Peri};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::task]
async fn blinky(led: Peri<'static, peripherals::PB1>) {
    let mut led = Output::new(led, Level::High, Speed::Low);

    loop {
        led.set_high();
        Timer::after_millis(50).await;

        led.set_low();
        Timer::after_millis(50).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.PB1)));

    let ch1_pin = PwmPin::new(p.PA8, OutputType::PushPull);
    let mut pwm = SimplePwm::new(
        p.TIM1,
        Some(ch1_pin),
        None,
        None,
        None,
        khz(1),
        Default::default(),
    );
    pwm.ch1().enable();
    let d = pwm.ch1().max_duty_cycle() / 3;
    pwm.ch1().set_duty_cycle(d);

    let ch1 = CapturePin::new(p.PA0, Pull::None);
    let mut ic = InputCapture::new(
        p.TIM2,
        Some(ch1),
        None,
        None,
        None,
        Irqs,
        mhz(1),
        Default::default(),
    );

    let mut dma = p.DMA1_CH1;

    let mut buffer = [0u16; 64];

    let mut counter = 0;

    loop {
        ic.capture_sequence::<Ch1>(
            dma.reborrow(),
            Channel::Ch1,
            &mut buffer,
            InputCaptureMode::BothEdges,
            FilterValue::NO_FILTER,
        )
        .await;

        info!("Iteration #{}", counter);
        for t in buffer {
            info!("timestamp={}", t);
        }

        counter += 1;
        Timer::after_millis(1000).await;
    }
}

Example output

2032.939331 [INFO ] Iteration #1824 
2032.940765 [INFO ] timestamp=542 
2032.942016 [INFO ] timestamp=875
2032.943267 [INFO ] timestamp=1542
2032.944519 [INFO ] timestamp=1875
2032.945770 [INFO ] timestamp=2542
2032.947021 [INFO ] timestamp=2875
...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant