From 8c2a6df03b852233ef6c774896cbb00c2a15040f Mon Sep 17 00:00:00 2001 From: eZio Pan Date: Thu, 28 Dec 2023 16:23:47 +0800 Subject: implement PWM waveform generating with DMA --- examples/stm32f4/src/bin/pwm.rs | 12 ++- examples/stm32f4/src/bin/ws2812_pwm.rs | 101 +++++++++++++++++++ examples/stm32f4/src/bin/ws2812_pwm_dma.rs | 150 ----------------------------- examples/stm32g4/src/bin/pwm.rs | 12 ++- examples/stm32h7/src/bin/pwm.rs | 13 ++- 5 files changed, 134 insertions(+), 154 deletions(-) create mode 100644 examples/stm32f4/src/bin/ws2812_pwm.rs delete mode 100644 examples/stm32f4/src/bin/ws2812_pwm_dma.rs (limited to 'examples') diff --git a/examples/stm32f4/src/bin/pwm.rs b/examples/stm32f4/src/bin/pwm.rs index 8844a9f0e..92bc42ec8 100644 --- a/examples/stm32f4/src/bin/pwm.rs +++ b/examples/stm32f4/src/bin/pwm.rs @@ -3,6 +3,7 @@ use defmt::*; use embassy_executor::Spawner; +use embassy_stm32::dma; use embassy_stm32::gpio::OutputType; use embassy_stm32::time::khz; use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; @@ -16,7 +17,16 @@ async fn main(_spawner: Spawner) { info!("Hello World!"); let ch1 = PwmPin::new_ch1(p.PE9, OutputType::PushPull); - let mut pwm = SimplePwm::new(p.TIM1, Some(ch1), None, None, None, khz(10), Default::default()); + let mut pwm = SimplePwm::new( + p.TIM1, + Some(ch1), + None, + None, + None, + khz(10), + Default::default(), + dma::NoDma, + ); let max = pwm.get_max_duty(); pwm.enable(Channel::Ch1); diff --git a/examples/stm32f4/src/bin/ws2812_pwm.rs b/examples/stm32f4/src/bin/ws2812_pwm.rs new file mode 100644 index 000000000..973743e49 --- /dev/null +++ b/examples/stm32f4/src/bin/ws2812_pwm.rs @@ -0,0 +1,101 @@ +// Configure TIM3 in PWM mode, and start DMA Transfer(s) to send color data into ws2812. +// We assume the DIN pin of ws2812 connect to GPIO PB4, and ws2812 is properly powered. +// +// The idea is that the data rate of ws2812 is 800 kHz, and it use different duty ratio to represent bit 0 and bit 1. +// Thus we can set TIM overflow at 800 kHz, and change duty ratio of TIM to meet the bit representation of ws2812. +// +// you may also want to take a look at `ws2812_spi.rs` file, which make use of SPI instead. +// +// Warning: +// DO NOT stare at ws2812 directy (especially after each MCU Reset), its (max) brightness could easily make your eyes feel burn. + +#![no_std] +#![no_main] + +use embassy_executor::Spawner; +use embassy_stm32::gpio::OutputType; +use embassy_stm32::time::khz; +use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; +use embassy_stm32::timer::{Channel, CountingMode}; +use embassy_time::{Duration, Ticker, Timer}; +use {defmt_rtt as _, panic_probe as _}; + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut device_config = embassy_stm32::Config::default(); + + // set SYSCLK/HCLK/PCLK2 to 20 MHz, thus each tick is 0.05 us, + // and ws2812 timings are integer multiples of 0.05 us + { + use embassy_stm32::rcc::*; + use embassy_stm32::time::*; + device_config.enable_debug_during_sleep = true; + device_config.rcc.hse = Some(Hse { + freq: mhz(12), + mode: HseMode::Oscillator, + }); + device_config.rcc.pll_src = PllSource::HSE; + device_config.rcc.pll = Some(Pll { + prediv: PllPreDiv::DIV6, + mul: PllMul::MUL80, + divp: Some(PllPDiv::DIV8), + divq: None, + divr: None, + }); + device_config.rcc.sys = Sysclk::PLL1_P; + } + + let dp = embassy_stm32::init(device_config); + + let mut ws2812_pwm = SimplePwm::new( + dp.TIM3, + Some(PwmPin::new_ch1(dp.PB4, OutputType::PushPull)), + None, + None, + None, + khz(800), // data rate of ws2812 + CountingMode::EdgeAlignedUp, + dp.DMA1_CH2, + ); + + // construct ws2812 non-return-to-zero (NRZ) code bit by bit + // ws2812 only need 24 bits for each LED, but we add one bit more to keep PWM output low + + let max_duty = ws2812_pwm.get_max_duty(); + let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing + let n1 = 2 * n0; // ws2812 Bit 1 high level timing + + let turn_off = [ + n0, n0, n0, n0, n0, n0, n0, n0, // Green + n0, n0, n0, n0, n0, n0, n0, n0, // Red + n0, n0, n0, n0, n0, n0, n0, n0, // Blue + 0, // keep PWM output low after a transfer + ]; + + let dim_white = [ + n0, n0, n0, n0, n0, n0, n1, n0, // Green + n0, n0, n0, n0, n0, n0, n1, n0, // Red + n0, n0, n0, n0, n0, n0, n1, n0, // Blue + 0, // keep PWM output low after a transfer + ]; + + let color_list = &[&turn_off, &dim_white]; + + let pwm_channel = Channel::Ch1; + + // make sure PWM output keep low on first start + ws2812_pwm.set_duty(pwm_channel, 0); + + // flip color at 2 Hz + let mut ticker = Ticker::every(Duration::from_millis(500)); + + loop { + for &color in color_list { + ws2812_pwm.gen_waveform(Channel::Ch1, color).await; + // ws2812 need at least 50 us low level input to confirm the input data and change it's state + Timer::after_micros(50).await; + // wait until ticker tick + ticker.next().await; + } + } +} diff --git a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs deleted file mode 100644 index 4458b643f..000000000 --- a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Configure TIM3 in PWM mode, and start DMA Transfer(s) to send color data into ws2812. -// We assume the DIN pin of ws2812 connect to GPIO PB4, and ws2812 is properly powered. -// -// The idea is that the data rate of ws2812 is 800 kHz, and it use different duty ratio to represent bit 0 and bit 1. -// Thus we can set TIM overflow at 800 kHz, and let TIM Update Event trigger a DMA transfer, then let DMA change CCR value, -// such that pwm duty ratio meet the bit representation of ws2812. -// -// You may want to modify TIM CCR with Cortex core directly, -// but according to my test, Cortex core will need to run far more than 100 MHz to catch up with TIM. -// Thus we need to use a DMA. -// -// This demo is a combination of HAL, PAC, and manually invoke `dma::Transfer`. -// If you need a simpler way to control ws2812, you may want to take a look at `ws2812_spi.rs` file, which make use of SPI. -// -// Warning: -// DO NOT stare at ws2812 directy (especially after each MCU Reset), its (max) brightness could easily make your eyes feel burn. - -#![no_std] -#![no_main] - -use embassy_executor::Spawner; -use embassy_stm32::gpio::OutputType; -use embassy_stm32::pac; -use embassy_stm32::time::khz; -use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; -use embassy_stm32::timer::{Channel, CountingMode}; -use embassy_time::{Duration, Ticker, Timer}; -use {defmt_rtt as _, panic_probe as _}; - -#[embassy_executor::main] -async fn main(_spawner: Spawner) { - let mut device_config = embassy_stm32::Config::default(); - - // set SYSCLK/HCLK/PCLK2 to 20 MHz, thus each tick is 0.05 us, - // and ws2812 timings are integer multiples of 0.05 us - { - use embassy_stm32::rcc::*; - use embassy_stm32::time::*; - device_config.enable_debug_during_sleep = true; - device_config.rcc.hse = Some(Hse { - freq: mhz(12), - mode: HseMode::Oscillator, - }); - device_config.rcc.pll_src = PllSource::HSE; - device_config.rcc.pll = Some(Pll { - prediv: PllPreDiv::DIV6, - mul: PllMul::MUL80, - divp: Some(PllPDiv::DIV8), - divq: None, - divr: None, - }); - device_config.rcc.sys = Sysclk::PLL1_P; - } - - let mut dp = embassy_stm32::init(device_config); - - let mut ws2812_pwm = SimplePwm::new( - dp.TIM3, - Some(PwmPin::new_ch1(dp.PB4, OutputType::PushPull)), - None, - None, - None, - khz(800), // data rate of ws2812 - CountingMode::EdgeAlignedUp, - ); - - // construct ws2812 non-return-to-zero (NRZ) code bit by bit - // ws2812 only need 24 bits for each LED, but we add one bit more to keep PWM output low - - let max_duty = ws2812_pwm.get_max_duty(); - let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing - let n1 = 2 * n0; // ws2812 Bit 1 high level timing - - let turn_off = [ - n0, n0, n0, n0, n0, n0, n0, n0, // Green - n0, n0, n0, n0, n0, n0, n0, n0, // Red - n0, n0, n0, n0, n0, n0, n0, n0, // Blue - 0, // keep PWM output low after a transfer - ]; - - let dim_white = [ - n0, n0, n0, n0, n0, n0, n1, n0, // Green - n0, n0, n0, n0, n0, n0, n1, n0, // Red - n0, n0, n0, n0, n0, n0, n1, n0, // Blue - 0, // keep PWM output low after a transfer - ]; - - let color_list = &[&turn_off, &dim_white]; - - let pwm_channel = Channel::Ch1; - - // PAC level hacking, enable output compare preload - // keep output waveform integrity - pac::TIM3 - .ccmr_output(pwm_channel.index()) - .modify(|v| v.set_ocpe(0, true)); - - // make sure PWM output keep low on first start - ws2812_pwm.set_duty(pwm_channel, 0); - - { - use embassy_stm32::dma::{Burst, FifoThreshold, Transfer, TransferOptions}; - - // configure FIFO and MBURST of DMA, to minimize DMA occupation on AHB/APB - let mut dma_transfer_option = TransferOptions::default(); - dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full); - dma_transfer_option.mburst = Burst::Incr8; - - // flip color at 2 Hz - let mut ticker = Ticker::every(Duration::from_millis(500)); - - loop { - for &color in color_list { - // start PWM output - ws2812_pwm.enable(pwm_channel); - - // PAC level hacking, enable timer-update-event trigger DMA - pac::TIM3.dier().modify(|v| v.set_ude(true)); - - unsafe { - Transfer::new_write( - // with &mut, we can easily reuse same DMA channel multiple times - &mut dp.DMA1_CH2, - 5, - color, - pac::TIM3.ccr(pwm_channel.index()).as_ptr() as *mut _, - dma_transfer_option, - ) - .await; - - // Turn off timer-update-event trigger DMA as soon as possible. - // Then clean the FIFO Error Flag if set. - pac::TIM3.dier().modify(|v| v.set_ude(false)); - if pac::DMA1.isr(0).read().feif(2) { - pac::DMA1.ifcr(0).write(|v| v.set_feif(2, true)); - } - - // ws2812 need at least 50 us low level input to confirm the input data and change it's state - Timer::after_micros(50).await; - } - - // stop PWM output for saving some energy - ws2812_pwm.disable(pwm_channel); - - // wait until ticker tick - ticker.next().await; - } - } - } -} diff --git a/examples/stm32g4/src/bin/pwm.rs b/examples/stm32g4/src/bin/pwm.rs index d4809a481..9fa004c3e 100644 --- a/examples/stm32g4/src/bin/pwm.rs +++ b/examples/stm32g4/src/bin/pwm.rs @@ -3,6 +3,7 @@ use defmt::*; use embassy_executor::Spawner; +use embassy_stm32::dma; use embassy_stm32::gpio::OutputType; use embassy_stm32::time::khz; use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; @@ -16,7 +17,16 @@ async fn main(_spawner: Spawner) { info!("Hello World!"); let ch1 = PwmPin::new_ch1(p.PC0, OutputType::PushPull); - let mut pwm = SimplePwm::new(p.TIM1, Some(ch1), None, None, None, khz(10), Default::default()); + let mut pwm = SimplePwm::new( + p.TIM1, + Some(ch1), + None, + None, + None, + khz(10), + Default::default(), + dma::NoDma, + ); let max = pwm.get_max_duty(); pwm.enable(Channel::Ch1); diff --git a/examples/stm32h7/src/bin/pwm.rs b/examples/stm32h7/src/bin/pwm.rs index 1e48ba67b..de155fc94 100644 --- a/examples/stm32h7/src/bin/pwm.rs +++ b/examples/stm32h7/src/bin/pwm.rs @@ -7,7 +7,7 @@ use embassy_stm32::gpio::OutputType; use embassy_stm32::time::khz; use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; use embassy_stm32::timer::Channel; -use embassy_stm32::Config; +use embassy_stm32::{dma, Config}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; @@ -38,7 +38,16 @@ async fn main(_spawner: Spawner) { info!("Hello World!"); let ch1 = PwmPin::new_ch1(p.PA6, OutputType::PushPull); - let mut pwm = SimplePwm::new(p.TIM3, Some(ch1), None, None, None, khz(10), Default::default()); + let mut pwm = SimplePwm::new( + p.TIM3, + Some(ch1), + None, + None, + None, + khz(10), + Default::default(), + dma::NoDma, + ); let max = pwm.get_max_duty(); pwm.enable(Channel::Ch1); -- cgit