Skip to content
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

PWM DMA support #350

Open
fishrockz opened this issue Jul 28, 2021 · 6 comments
Open

PWM DMA support #350

fishrockz opened this issue Jul 28, 2021 · 6 comments

Comments

@fishrockz
Copy link
Contributor

I notice that this repo dose not allow for DMA transfers to the TimX devices.

I recently followed https://stackoverflow.com/questions/63016570/how-to-make-dma-work-for-changing-the-duty-cycle-of-a-pwm-port-using-rust and i seem to have some thing very similar to that example working but for stm32f1, I have tested on a blue pill and get some cool wave forms out.

I will keep working on my example to create wave forms for my use case and will be trying to significantly reduce the unsafe code.

I would like to write this up, ideally as a addition to this lib. but I'm not sure what API to expose to users. I have not seen any other repos in https://github.com/stm32-rs that have DMA for there pwm.rs files other wise i would have just borrowed from one of them.

Is there any resources I could lean on when creating a PR or is there a reason that this functionality dose not already exist?

I notice that spi and other things use DMA but there API's are quite different so have struggled to use them as inspiration but at this point they are the best thing i have.

Many thanks

@burrbull
Copy link
Contributor

I've not heard about such implementation.
But you can ask in matrix channel: https://app.element.io/#/room/#rust-embedded:matrix.org

@alexmadeathing
Copy link

Did you make any progress with this? I also need DMA to PWM output.

@fishrockz
Copy link
Contributor Author

Oh, yer I have quite a nice little motor controler for the one shot protical but I never made any progress in upstreaming it.

@alexmadeathing
Copy link

I'm hoping to DMA to PWM duty cycle so that I can drive WS2812 asynchronously since they're vey time sensitive (synchronous timing is getting messed up by my other interrupts). Does that sound like the same sort of situation you overcame with your motors?

Have you got some code I can use as a reference? No worries if not!

@fishrockz
Copy link
Contributor Author

fishrockz commented Mar 3, 2024

I think this is the main bit to get the DMA going for PWM

note that i was creating a pulse chain ie, set it off and then have several different pulses all spat out one after each other

    let c41 = gpiob.pb6.into_alternate_push_pull(&mut gpiob.crl);
    let c42 = gpiob.pb7.into_alternate_push_pull(&mut gpiob.crl);
    let c43 = gpiob.pb8.into_alternate_push_pull(&mut gpiob.crh);
    let c44 = gpiob.pb9.into_alternate_push_pull(&mut gpiob.crh);
    // If you don't want to use all channels, just leave some out
    // let c4 = gpioa.pa3.into_alternate_push_pull(&mut gpioa.crl);
    let pins4 = (c41, c42, c43, c44);

    let mut pwm4 =
        Timer::tim4(dp.TIM4, &clocks).pwm::<Tim4NoRemap, _, _, _>(pins4, &mut afio.mapr, 50.hz());

    let max4 = pwm4.get_max_duty();

    pwm4.set_duty(Channel::C1, 0);
    pwm4.enable(Channel::C1);

    // TIM2
    let c1 = gpioa.pa0.into_alternate_push_pull(&mut gpioa.crl);
    let c2 = gpioa.pa1.into_alternate_push_pull(&mut gpioa.crl);
    let c3 = gpioa.pa2.into_alternate_push_pull(&mut gpioa.crl);
    let c4 = gpioa.pa3.into_alternate_push_pull(&mut gpioa.crl);
    // If you don't want to use all channels, just leave some out
    // let c4 = gpioa.pa3.into_alternate_push_pull(&mut gpioa.crl);
    let pins = (c1, c2, c3, c4);

    let mut pwm = Timer::tim2(dp.TIM2, &clocks).pwm::<Tim2NoRemap, _, _, _>(
        pins,
        &mut afio.mapr,
        //1_250.khz(),
        625.khz(),
    );

    let max = pwm.get_max_duty();
    hprintln!("max {}", max).unwrap();

    pwm.set_duty(Channel::C1, 0);
    pwm.enable(Channel::C1);

    let tim2 = unsafe { &(*pac::TIM2::ptr()) }; // general timer 2 pointer
    let rcc_ptr = unsafe { &*pac::RCC::ptr() }; // RCC pointer
    let dma1 = unsafe { &*pac::DMA1::ptr() }; // DMA 1 pointer

    rcc_ptr.ahbenr.modify(|_, w| w.dma1en().set_bit()); // enable DMA1 clock: peripheral clock enable register

    // timer for DMA configuration
    tim2.dier.write(|w| w.tde().set_bit()); // enable DMA trigger
    tim2.dier.write(|w| w.ude().set_bit()); // enable update DMA request

    let _a = &tim2.ccr1 as *const _ as u32; // very different from 0x4000_0034

    // DMA configuration
    unsafe {
        dma1.ch2.par.write(|w| {
            w.pa()
                .bits(unsafe { &(*pac::TIM2::ptr()).dcr } as *const _ as u32)
        });
    };

    //dma1.cselr.write(|w| w.c2s().bits(0b0100)); // set CxS[3:0] to 0100 to map the DMA request to timer 2 channel 1
    //dma1.cselr.write(|w| w.c2s().bits(0b0100)); // set CxS[3:0] to 0100 to map the DMA request to timer 2 channel 1
    //dma1.cpar2.write(|w| w.pa().bits(0x4000_0034)); // set the DMA peripheral address register to the capture/compare 1 of TIM2
    unsafe {
        dma1.ch2.par.write(|w| w.pa().bits(0x4000_0034));
    }
    //dma1.cmar2.write(|w| w.ma().bits(buffer.as_ptr() as u32)); // write the buffer to the memory adress
    unsafe {
        dma1.ch2
            .mar
            .write(|w| w.ma().bits(unsafe { GLOBAL_BUFF.as_ptr() } as u32));
    }
    //dma1.cndtr2.write(|w| w.ndt().bits(buffer.len() as u16)); // number of data to transfer register
    dma1.ch2.ndtr.write(|w| {
        w.ndt()
            .bits(u16::try_from(unsafe { GLOBAL_BUFF.len() }).unwrap())
    });
    //dma1.ccr2.modify(|_, w| w
    dma1.ch2.cr.modify(
        |_, w| {
            w.mem2mem()
                .clear_bit() // memory-to-memory disabled
                .pl()
                .high() // set highest priority
                .msize()
                .bits16() // size in memory of each transfer: b10 = 32 bits long
                .psize()
                .bits16() // size of peripheral: b10 = 32 bits long --> 32 or 16 ??
                .minc()
                .set_bit() // memory increment mode enabled
                .pinc()
                .clear_bit() // peripheral increment mode disabled
                .circ()
                .clear_bit() //.set_bit() // circular mode: the dma transfer is repeated automatically when finished
                .dir()
                .set_bit() // data transfer direction: 1 = read from memory
                .teie()
                .set_bit() // transfer error interrupt enabled
                //.htie().set_bit() // half transfer interrupt enabled
                .tcie()
                .set_bit() // transfer complete interrupt enabled
                .en()
                .set_bit()
        }, // channel enable
    );

you need something to fill the buffer back up when the dma gets to the end

#[interrupt]
fn DMA1_CHANNEL2() {
    unsafe { COUNT += 1 };

    let dma1 = unsafe { &*pac::DMA1::ptr() };
    dma1.ifcr.write(|w| w.cgif2().set_bit());
    //clear the interrupt so this function dose not fire until after the next pulse
    write_times(unsafe { next_command }, unsafe { need_telem }, unsafe {
        &mut GLOBAL_BUFF
    });
    unsafe { need_telem = 0 };
    dma1.ch2.cr.modify(|_, w| w.en().clear_bit());

    dma1.ch2.ndtr.write(|w| {
        w.ndt()
            .bits(cast::u16(unsafe { GLOBAL_BUFF.len() }).unwrap())
    });
    dma1.ch2.cr.modify(|_, w| w.en().set_bit());
}

You will also need some state that your main loop and interupt can access and a function to change it

static mut GLOBAL_BUFF: [u16; 66] = [0; 66];

fn write_times(new_command: u16, telm: u16, buffer: &mut [u16]) {
    let max = 76;
    let one_duty = (max * 125 / 167) as u16;
    let zero_duty = (max * 62 / 167) as u16;

    let clean_command = new_command;
    let mut data: u16 = ((clean_command & 0b11111111111) << 5) | ((telm & 0b1) << 4);
    let mut checksum = (data >> 4) ^ (data >> 8) ^ (data >> 12);
    //if (telm & 0b1) == 1 {
    //    checksum = !checksum;
    //}
    checksum = checksum & 0x0f;
    //}
    data = data | checksum;

    for iii in 0..16 {
        let val: u16 = data & (1 << (15 - iii));
        if val != 0 {
            buffer[iii] = one_duty;
        } else {
            buffer[iii] = zero_duty;
        }
    }
}

I think a later version of this was a bit less unsafe but you do have to be careful about memory and lock and avoid dead locks etc

you have to be careful cos the interrupt could go off at any point in you main loop so you generally need something to co ordinate, can be as simple as turning off interrupts when you update GLOBAL_BUFF or what ever you use handle state shared between the interrupt and the main loop

@alexmadeathing
Copy link

alexmadeathing commented Mar 4, 2024

Wow, you went above and beyond here. Thanks! Yes, I'll be using a pulse train too. I think my requirements are very similar. I'm using RTIC, so I'll just lean on that for resource sharing and interrupt free zones.

Why do you set TIM4 to 50hz early on? Is that just so you can get access to the PWM channel and then DMA with TIM2 takes over? [EDIT - Never mind. I see you've got separate PWMs on TIM2 and 4.]

I'll leave you be now. You've given me more than enough to chew on. Thanks again.

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

3 participants