Skip to content

Question: can I use just the complementary PWM of a timer/channel? #195

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
0x53A opened this issue Apr 29, 2025 · 5 comments
Open

Question: can I use just the complementary PWM of a timer/channel? #195

0x53A opened this issue Apr 29, 2025 · 5 comments

Comments

@0x53A
Copy link

0x53A commented Apr 29, 2025

I have an existing board with a STM32G431Rx. I need to drive a PWM at pin C11. C11 is the complementary output of Timer8_Channel2.

Additionally, I'd like to drive a PWM at pin B6. B6 is Timer8_Channel1.

Driving only B6 should be easy:

    let b6 = gpiob.pb6.into_alternate::<AF5>();
    let mut pwm = dp.TIM8.pwm(b6, 1_000_u32.Hz(), &mut rcc);

I'm struggling to drive just C11.

    let c11 = gpioc.pc11.into_alternate::<AF4>();
    let mut pwm = dp.TIM8.pwm(c11, 1_000_u32.Hz(), &mut rcc);
error[E0277]: the trait bound `PC11<Alternate<4>>: Pins<Periph<RegisterBlock, 1073820672>, _, _>` is not satisfied
    --> src/main.rs:438:31
     |
438  |     let mut pwm = dp.TIM8.pwm(c11, 1_000_u32.Hz(), &mut rcc);
     |                           --- ^^^ unsatisfied trait bound
     |                           |
     |                           required by a bound introduced by this call

It compiles when I use one of the primary pins for Tim8Ch2:

    let c11 = gpioc.pc11.into_alternate::<AF4>();
    let pin_dummy_pwm = gpioc.pc7.into_alternate();

    let mut pwm = dp.TIM8.pwm(pin_dummy_pwm, 1_000_u32.Hz(), &mut rcc);
    pwm.into_complementary(c11);

As I understand it, to use multiple channels of the same timer, I'll need to use tuple syntax. The following compiles (I haven't yet tested any of this on hardware):

    let b6 = gpiob.pb6.into_alternate::<AF5>();
    let c11 = gpioc.pc11.into_alternate::<AF4>();
    let pin_dummy_pwm = gpioc.pc7.into_alternate();
    
    let (mut pwm1, mut pwm2) = dp.TIM8.pwm((b6, pin_dummy_pwm), 1_000_u32.Hz(), &mut rcc);
    pwm2.into_complementary(c11);

Unfortunately I actually do need both possible primary pins of timer 8 channel 2 (C7 or A14) for other functionality so I can't just leave them floating and run a dummy PWM on them.

So is it possible to run just the complementary signal, without having to configure the primary pin?

@0x53A
Copy link
Author

0x53A commented Apr 30, 2025

Just as context, I got it working by falling back to direct register manipulation. So if special cases are out-of-scope for the nice api, then do feel free to close the issue.

(code was ai generated)

let tim8 = dp.TIM8;
    // Configure TIM8 from scratch using registers
    unsafe {
        // Enable TIM8 clock
        (*stm32g4xx_hal::pac::RCC::ptr())
            .apb2enr()
            .modify(|_, w| w.tim8en().set_bit());

        // Reset timer
        tim8.cr1().write(|w| w.bits(0));

        // Set prescaler (adjust frequency as needed)
        tim8.psc().write(|w| {
            w.psc()
                .bits((rcc.clocks.sys_clk.to_Hz() / 1_000_000 - 1) as u16)
        });

        // Set auto-reload (period) for 1kHz
        tim8.arr().write(|w| w.arr().bits(1000));

        // Configure channel 2 as PWM mode 1
        tim8.ccmr1_output().modify(
            |_, w| {
                w.oc2m()
                    .bits(0b110) // PWM mode 1
                    .oc2pe()
                    .set_bit()
            }, // Preload enable
        );

        // Set output compare register (duty cycle)
        tim8.ccr2().write(|w| w.ccr().bits(0)); // Start with 0% duty

        // Enable complementary output and configure polarity
        tim8.ccer().modify(
            |_, w| {
                w.cc2e()
                    .clear_bit() // Disable main output
                    // note: complementary output is enabled later - here the PWM is just configured but not yet started
                    // .cc2ne()
                    // .set_bit() // Enable complementary output
                    .cc2p()
                    .clear_bit() // Main output polarity (not used)
                    .cc2np()
                    .clear_bit()
            }, // Complementary output polarity: active high (reversed)
        );

        // Configure break and dead-time register
        tim8.bdtr().modify(
            |_, w| {
                w.moe()
                    .set_bit() // Main output enable
                    .aoe()
                    .set_bit() // Automatic output enable
                    .ossr()
                    .set_bit() // Off-state selection for Run mode
                    .ossi()
                    .set_bit() // Off-state selection for Idle mode
                    .dtg()
                    .bits(0)
            }, // No dead time
        );

        // Generate update event to load registers
        tim8.egr().write(|w| w.ug().set_bit());

        // Enable counter
        tim8.cr1().modify(|_, w| w.cen().set_bit());
    }

    unsafe {
        set_tim8_ch2n_duty(&tim8, 900);

        enable_tim8_ch2n_pwm(&tim8);
    }

// -----------


unsafe fn set_tim8_ch2n_duty(tim8: &stm32g4xx_hal::stm32::TIM8, duty: u16) {
    let duty = duty.min(1000);
    tim8.ccr2().write(|w| w.ccr().bits(duty as u32));
}

/// Enables PWM output on TIM8 CH2N
unsafe fn enable_tim8_ch2n_pwm(tim8: &stm32g4xx_hal::stm32::TIM8) {
    // Enable complementary output for channel 2
    tim8.ccer().modify(|_, w| w.cc2ne().set_bit());
    
    // Ensure main output is enabled in the BDTR register
    // (This is a global enable for all channels)
    tim8.bdtr().modify(|_, w| w.moe().set_bit());
}

/// Disables PWM output on TIM8 CH2N
unsafe fn disable_tim8_ch2n_pwm(tim8: &stm32g4xx_hal::stm32::TIM8) {
    // Disable complementary output for channel 2
    tim8.ccer().modify(|_, w| w.cc2ne().clear_bit());
}

@usbalbin
Copy link
Member

Quick thought (without having actually looked at the timer code in a while), perhaps we could implement Pin for () or NoPin or something similar. What do you think about that?

@0x53A
Copy link
Author

0x53A commented Apr 30, 2025

Yeah that would make sense, you’d need to also be able to specify which channel to use.

@usbalbin
Copy link
Member

usbalbin commented May 1, 2025

Slightly modifying examples/pwm.rs:

 #[macro_use]
 mod utils;

+struct NoPin<CH>{
+    _ch: PhantomData<CH>
+}
+
+impl<CH> NoPin<CH> {
+    fn new() -> Self {
+        Self { _ch: PhantomData }
+    }
+}
+
+impl<CH, TIM> Pins<TIM, CH, ComplementaryDisabled> for NoPin<CH> {
+    type Channel = Pwm<TIM, CH, ComplementaryDisabled, ActiveHigh, ActiveHigh>;
+}
+
 #[entry]
 fn main() -> ! {
     utils::logger::init();
@@ -23,9 +41,10 @@ fn main() -> ! {
     let dp = stm32::Peripherals::take().expect("cannot take peripherals");
     let mut rcc = dp.RCC.constrain();
     let gpioa = dp.GPIOA.split(&mut rcc);
-    let pin: PA8<Alternate<AF6>> = gpioa.pa8.into_alternate();
+    let pa7 = gpioa.pa7.into_alternate(); // tim1_ch1n/tim8_ch1n

-    let mut pwm = dp.TIM1.pwm(pin, 100.Hz(), &mut rcc);
+    let mut pwm = dp.TIM1.pwm(NoPin::<C1>::new(), 100.Hz(), &mut rcc).into_complementary(pa7);

     let _ = pwm.set_duty_cycle_percent(50);
     pwm.enable();

I have not tested this on real hardware

@0x53A
Copy link
Author

0x53A commented May 7, 2025

Thanks! That works perfectly, and together with into_comp_active_low I also do not need to reverse the duty cycle.

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

No branches or pull requests

2 participants