David-OConnor / stm32-hal

This library provides access to STM32 peripherals in Rust.
MIT License
171 stars 45 forks source link

PWM DMA burst #107

Closed dkotTech closed 1 month ago

dkotTech commented 4 months ago

Hi, I am working on an example that demonstrates the usage of a DRV8844 motor driver with PWM and DMA.

However, something is not working as expected. The duty cycle does not change after a DMA write request.

full src link

code:


#![no_main]
#![no_std]

use defmt_rtt as _;
use panic_probe as _;

use hal::{
    self,
    clocks::Clocks,
    dma,
    dma::{ChannelCfg, Dma, DmaChannel, DmaInput, DmaPeriph},
    gpio::{Pin, PinMode, Port},
    pac,
    pac::{TIM2, TIM3},
    timer::{
        OutputCompare, TimChannel, Timer, TimerConfig,
        TimerInterrupt,
    },
};
pub static BUF: [u16; 4] = [1, 2, 3, 4];

#[rtic::app(device = pac, peripherals = true)]
mod app {
    use super::*;

    #[shared]
    struct Shared {}

    #[local]
    struct Local {
        timer_pwd: Timer<TIM2>,
        timer: Timer<TIM3>,
    }

    #[init]
    fn init(ctx: init::Context) -> (Shared, Local) {
        let dp = ctx.device;

        let clock_cfg = Clocks::default();
        clock_cfg.setup().unwrap();

        Pin::new(Port::A, 0, PinMode::Alt(1));

        let mut dma = Dma::new(dp.DMA1);
        dma::mux(DmaPeriph::Dma1, DmaChannel::C1, DmaInput::Tim2Up);

        let mut timer_pwd = Timer::new_tim2(
            dp.TIM2,
            20_000.,
            TimerConfig::default(),
            &clock_cfg,
        );

        timer_pwd.enable_pwm_output(TimChannel::C1, OutputCompare::Pwm1, 0.1);

        timer_pwd.enable_interrupt(TimerInterrupt::UpdateDma);

        unsafe {
            timer_pwd.write_dma_burst(
                &BUF,
                13,
                4,
                DmaChannel::C1,
                ChannelCfg ::default(),
                true,
                DmaPeriph::Dma1,
            );
        }

        let mut timer = Timer::new_tim3(dp.TIM3, 0.2, Default::default(), &clock_cfg);
        timer.enable_interrupt(TimerInterrupt::Update);
        timer.enable();

        (Shared {}, Local { timer_pwd, timer })
    }

    #[task(binds = TIM3, local=[timer_pwd, timer, a: i32 = 0], priority = 1)]
    fn on_dma1_complete(cx: on_dma1_complete::Context) {
        cx.local.timer.clear_interrupt(TimerInterrupt::Update);

        defmt::println!("psc: {:?}", cx.local.timer_pwd.regs.psc.read().bits());
        defmt::println!("arr: {:?}", cx.local.timer_pwd.regs.arr.read().bits());
        defmt::println!("rcr: {:?}", cx.local.timer_pwd.regs.rcr.read().bits());

        defmt::println!(
            "cx.local.timer_pwd1: {:?}",
            cx.local.timer_pwd.get_duty(TimChannel::C1)
        );
        defmt::println!(
            "cx.local.timer_pwd2: {:?}",
            cx.local.timer_pwd.get_duty(TimChannel::C2)
        );
        defmt::println!(
            "cx.local.timer_pwd3: {:?}",
            cx.local.timer_pwd.get_duty(TimChannel::C3)
        );
        defmt::println!(
            "cx.local.timer_pwd4: {:?}",
            cx.local.timer_pwd.get_duty(TimChannel::C4)
        );
    }
}

#[defmt::panic_handler]
fn panic() -> ! {
    cortex_m::asm::udf()
}

cargo:

[package]
name = "pwm-dma-example"
version = "0.1.0"
edition = "2021"

[dependencies]
defmt = "0.3.4"
defmt-rtt = "0.4.0"
panic-probe = { version = "0.3.1", features = ["print-defmt"] }

cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
hal = { package = "stm32-hal2", version = "^1.8.3", features = ["g431", "g4rt"]}
rtic = { version = "2.1.1", features = ["cortex-m", "thumbv7-backend", "rtic-monotonics"] }

output:

<lvl> psc: 0
└─ dma_pwm::app::on_dma1_complete @ dma_pwm/src/main.rs:82  
<lvl> arr: 8499
└─ dma_pwm::app::on_dma1_complete @ dma_pwm/src/main.rs:83  
<lvl> rcr: 0
└─ dma_pwm::app::on_dma1_complete @ dma_pwm/src/main.rs:84  
<lvl> cx.local.timer_pwd1: 849
└─ dma_pwm::app::on_dma1_complete @ dma_pwm/src/main.rs:86  
<lvl> cx.local.timer_pwd2: 0
└─ dma_pwm::app::on_dma1_complete @ dma_pwm/src/main.rs:90  
<lvl> cx.local.timer_pwd3: 0
└─ dma_pwm::app::on_dma1_complete @ dma_pwm/src/main.rs:94  
<lvl> cx.local.timer_pwd4: 0
└─ dma_pwm::app::on_dma1_complete @ dma_pwm/src/main.rs:98 
akhilles commented 2 months ago

Is it possible that the issue is with measuring the duty cycle rather than generating the signal? In case it's useful, I have a working example of PWM DMA burst tested on a STM32H747I-DISCO. I wasn't able to get examples/timer.rs to compile.

image

#![no_main]
#![no_std]

use cortex_m::delay::Delay;
use hal::{
    clocks::Clocks,
    dma::{self, Dma, DmaChannel, DmaInput, DmaPeriph},
    gpio::{Pin, PinMode, Port},
    pac,
    timer::{OutputCompare, TimChannel, Timer, TimerConfig, TimerInterrupt},
};

use defmt_rtt as _;
use panic_probe as _;

// Calculate the DMA offset by taking the Adddress Offset for
// the associated CCR channel in the RM register table, and dividing by 4.
const TIMX_CCR1_OFFSET: u8 = 0x34;

// 10 duty cycles from 10% to 80% assuming max duty cycle is 5332
const DUTY_CYCLES: [u16; 8] = [533, 1066, 1600, 2133, 2666, 3199, 3732, 4266];

#[cortex_m_rt::entry]
fn main() -> ! {
    let cp = cortex_m::Peripherals::take().unwrap();
    let dp = pac::Peripherals::take().unwrap();
    let clock_cfg = Clocks::default();
    clock_cfg.setup().unwrap();

    let mut delay = Delay::new(cp.SYST, clock_cfg.systick());

    // Generate a PWM signal with 50% duty cycle and 2400 Hz frequency on PC6 (TIM3_CH1)
    // TIM3_CH1 is connected to PC6 as alternate function 2
    let _pwm_pin = Pin::new(Port::C, 6, PinMode::Alt(2));
    let mut pwm_timer = Timer::new_tim3(
        dp.TIM3,
        37_500.,
        TimerConfig::default(),
        &clock_cfg,
    );
    pwm_timer.enable_pwm_output(TimChannel::C1, OutputCompare::Pwm1, 0.5);

    let _dma = Dma::new(dp.DMA1);
    dma::mux(DmaPeriph::Dma1, DmaChannel::C1, DmaInput::Tim3Up);
    pwm_timer.enable_interrupt(TimerInterrupt::UpdateDma);

    loop {
        delay.delay_ms(2000);
        pwm_timer.enable();
        defmt::info!("Generating 50% duty cycle PWM signal");
        delay.delay_ms(5);

        // Set duty cycle using DMA
        unsafe {
            pwm_timer.write_dma_burst(
                &DUTY_CYCLES,
                TIMX_CCR1_OFFSET / 4,
                1,
                DmaChannel::C1,
                dma::ChannelCfg {
                    circular: dma::Circular::Enabled,
                    ..Default::default()
                },
                false,
                DmaPeriph::Dma1,
            );
        }
        defmt::info!("Generating 10% to 80% duty cycle PWM signal");
    }
}
David-OConnor commented 2 months ago

Hey! I don't know what's going on. That's possible. Timer shenanigans on STM32 are tricky! LMK if you come up with anything; would love to get this fixed.

dkotTech commented 2 months ago

@akhilles Hello, I tried your example, but it's not working for me either. Some parts of the code differ between the G4 and H7 chips. Anyway, I tried to set up the project in CubeIDE.

The HAL_TIM_DMABurst_WriteStart function works strangely—some values are transferred from DMA to the timer registers, but some are not. After several attempts, I still can't find the correct way to use this function.

The HAL_TIM_PWM_Start_DMA function works as expected. I can set a buffer for DMA, and DMA sends this buffer to CCRx in a circular manner.

David-OConnor commented 2 months ago

LMK if you come up with anything. I've used the burst DMA on G4 only; haven't tried on other MCUs. (And only for one specific use case that may not generalize) I suspect the fix is something simple, but I'm not sure what it is.

What I would do: See if you can get it working using the Reference manual and PAC directly, then post the results here, and/or compare to what the HAL functions are doing.

dkotTech commented 2 months ago

Hi! After a few hours of searching, I finally managed to achieve what I wanted, and even more. Now, I know two ways to solve the problem.

I’ll clean up the code and post it later.

So I tried to figure out what was wrong with the HAL library code but couldn't find anything suspicious. Everything looks correct, even the parts I had doubts about turned out fine.

In the end, just one line placed in the right spot fixed everything:

dma::enable_mux1();

I’m not sure why the mux doesn’t need to be enabled for the H7 chip; it seems to work differently. Also, note that the mux must be enabled before specifying the routing.

akhilles commented 2 months ago

Nice find! It might be a good idea to enable the DMA mux RCC clock for G4 targets automatically when calling dma::mux? It's just 1 register write, and setting up dma muxing if fairly infrequent.

David-OConnor commented 2 months ago

Nice find! I agree that it would be nice to enable it on G4 in dma::mux. Done; in the latest commit.

I also wish the docs could be easily hosted/built for all targets: The enable_mux fn is not in the published docs because it's only on G4.

dkotTech commented 1 month ago

Hi, thanks for the help.

If anyone has encountered a problem working with DMA, you can check out example with a deep dive into registers. https://github.com/dkotTech/stm32g431-exp/blob/main/dma_pwm_pac/src/main.rs