From 0a890cfbe7fc10bc40f2e97bc4fac17630e9864f Mon Sep 17 00:00:00 2001 From: eZio Pan Date: Sun, 17 Dec 2023 23:47:00 +0800 Subject: [PATCH 1/6] stm32f4 ws2812 example with spi ... ... and more doc on TIM&DMA version, also remove useless TIM APRE settings, and use for loop instead of manually flip the index bit, and replace `embassy_time::Timer` with `embassy_time::Ticker`, for more constant time interval. --- examples/stm32f4/src/bin/ws2812_pwm_dma.rs | 74 +++++++++-------- examples/stm32f4/src/bin/ws2812_spi.rs | 95 ++++++++++++++++++++++ 2 files changed, 135 insertions(+), 34 deletions(-) create mode 100644 examples/stm32f4/src/bin/ws2812_spi.rs diff --git a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs index 52cc665c7..dccd639ac 100644 --- a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs +++ b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs @@ -1,7 +1,16 @@ // 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. // -// This demo is a combination of HAL, PAC, and manually invoke `dma::Transfer` +// 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. @@ -16,7 +25,7 @@ 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::Timer; +use embassy_time::{Duration, Ticker, Timer}; use {defmt_rtt as _, panic_probe as _}; #[embassy_executor::main] @@ -33,7 +42,6 @@ async fn main(_spawner: Spawner) { freq: mhz(12), mode: HseMode::Oscillator, }); - device_config.rcc.sys = Sysclk::PLL1_P; device_config.rcc.pll_src = PllSource::HSE; device_config.rcc.pll = Some(Pll { prediv: PllPreDiv::DIV6, @@ -42,6 +50,7 @@ async fn main(_spawner: Spawner) { divq: None, divr: None, }); + device_config.rcc.sys = Sysclk::PLL1_P; } let mut dp = embassy_stm32::init(device_config); @@ -56,14 +65,11 @@ async fn main(_spawner: Spawner) { CountingMode::EdgeAlignedUp, ); - // PAC level hacking, - // enable auto-reload preload, and enable timer-update-event trigger DMA - { - pac::TIM3.cr1().modify(|v| v.set_arpe(true)); - pac::TIM3.dier().modify(|v| v.set_ude(true)); - } + // PAC level hacking, enable timer-update-event trigger DMA + pac::TIM3.dier().modify(|v| v.set_ude(true)); // 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 @@ -83,7 +89,7 @@ async fn main(_spawner: Spawner) { 0, // keep PWM output low after a transfer ]; - let color_list = [&turn_off, &dim_white]; + let color_list = &[&turn_off, &dim_white]; let pwm_channel = Channel::Ch1; @@ -98,34 +104,34 @@ async fn main(_spawner: Spawner) { dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full); dma_transfer_option.mburst = Burst::Incr8; - let mut color_list_index = 0; + // flip color at 2 Hz + let mut ticker = Ticker::every(Duration::from_micros(500)); loop { - // start PWM output - ws2812_pwm.enable(pwm_channel); + for &color in color_list { + // start PWM output + ws2812_pwm.enable(pwm_channel); - unsafe { - Transfer::new_write( - // with &mut, we can easily reuse same DMA channel multiple times - &mut dp.DMA1_CH2, - 5, - color_list[color_list_index], - pac::TIM3.ccr(pwm_channel.raw()).as_ptr() as *mut _, - dma_transfer_option, - ) - .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; + 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.raw()).as_ptr() as *mut _, + dma_transfer_option, + ) + .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; + } + + // stop PWM output for saving some energy + ws2812_pwm.disable(pwm_channel); + + // wait until ticker tick + ticker.next().await; } - - // stop PWM output for saving some energy - ws2812_pwm.disable(pwm_channel); - - // wait another half second, so that we can see color change - Timer::after_millis(500).await; - - // flip the index bit so that next round DMA transfer the other color data - color_list_index ^= 1; } } } diff --git a/examples/stm32f4/src/bin/ws2812_spi.rs b/examples/stm32f4/src/bin/ws2812_spi.rs new file mode 100644 index 000000000..e0d28af7f --- /dev/null +++ b/examples/stm32f4/src/bin/ws2812_spi.rs @@ -0,0 +1,95 @@ +// Mimic PWM with SPI, to control ws2812 +// We assume the DIN pin of ws2812 connect to GPIO PB5, 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 adjust SPI to send each *round* of data at 800 kHz, and in each *round*, we can adjust each *bit* to mimic 2 different PWM waveform. +// such that the output waveform meet the bit representation of ws2812. +// +// If you want to save SPI for other purpose, you may want to take a look at `ws2812_tim_dma.rs` file, which make use of TIM and DMA. +// +// 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] +#![feature(type_alias_impl_trait)] + +use embassy_stm32::time::khz; +use embassy_stm32::{dma, spi}; +use embassy_time::{Duration, Ticker, Timer}; +use {defmt_rtt as _, panic_probe as _}; + +// we use 16 bit data frame format of SPI, to let timing as accurate as possible. +// thanks to loose tolerance of ws2812 timing, you can also use 8 bit data frame format, thus you need want to adjust the bit representation. +const N0: u16 = 0b1111100000000000u16; // ws2812 Bit 0 high level timing +const N1: u16 = 0b1111111111000000u16; // ws2812 Bit 1 high level timing + +// ws2812 only need 24 bits for each LED, but we add one bit more to keep SPI output low + +static TURN_OFF: [u16; 25] = [ + 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 SPI output low after last bit +]; + +static DIM_WHITE: [u16; 25] = [ + 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 SPI output low after last bit +]; + +static COLOR_LIST: &[&[u16]] = &[&TURN_OFF, &DIM_WHITE]; + +#[embassy_executor::main] +async fn main(_spawner: embassy_executor::Spawner) { + let mut device_config = embassy_stm32::Config::default(); + + // Since we use 16 bit SPI, and we need each round 800 kHz, + // thus SPI output speed should be 800 kHz * 16 = 12.8 MHz, and APB clock should be 2 * 12.8 MHz = 25.6 MHz. + // + // As for my setup, with 12 MHz HSE, I got 25.5 MHz SYSCLK, which is slightly slower, but it's ok for ws2812. + { + use embassy_stm32::rcc::{Hse, HseMode, Pll, PllMul, PllPDiv, PllPreDiv, PllSource, Sysclk}; + use embassy_stm32::time::mhz; + 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::MUL102, + divp: Some(PllPDiv::DIV8), + divq: None, + divr: None, + }); + device_config.rcc.sys = Sysclk::PLL1_P; + } + + let dp = embassy_stm32::init(device_config); + + // Set SPI output speed. + // It's ok to blindly set frequency to 12800 kHz, the hal crate will take care of the SPI CR1 BR field. + // And in my case, the real bit rate will be 25.5 MHz / 2 = 12_750 kHz + let mut spi_config = spi::Config::default(); + spi_config.frequency = khz(12_800); + + // Since we only output waveform, then the Rx and Sck it is not considered + let mut ws2812_spi = spi::Spi::new_txonly_nosck(dp.SPI1, dp.PB5, dp.DMA2_CH2, dma::NoDma, spi_config); + + // flip color at 2 Hz + let mut ticker = Ticker::every(Duration::from_millis(500)); + + loop { + for &color in COLOR_LIST { + ws2812_spi.write(color).await.unwrap(); + // 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; + } + } +} From 1934c2abc81f0dd981fad625ec2712964d0a1a91 Mon Sep 17 00:00:00 2001 From: eZio Pan Date: Mon, 18 Dec 2023 00:06:32 +0800 Subject: [PATCH 2/6] match up with stm32f429zi feature flag stm32f429 has less DMA channel than stm32f411 --- examples/stm32f4/src/bin/ws2812_spi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/stm32f4/src/bin/ws2812_spi.rs b/examples/stm32f4/src/bin/ws2812_spi.rs index e0d28af7f..170f0b59b 100644 --- a/examples/stm32f4/src/bin/ws2812_spi.rs +++ b/examples/stm32f4/src/bin/ws2812_spi.rs @@ -78,7 +78,7 @@ async fn main(_spawner: embassy_executor::Spawner) { spi_config.frequency = khz(12_800); // Since we only output waveform, then the Rx and Sck it is not considered - let mut ws2812_spi = spi::Spi::new_txonly_nosck(dp.SPI1, dp.PB5, dp.DMA2_CH2, dma::NoDma, spi_config); + let mut ws2812_spi = spi::Spi::new_txonly_nosck(dp.SPI1, dp.PB5, dp.DMA2_CH3, dma::NoDma, spi_config); // flip color at 2 Hz let mut ticker = Ticker::every(Duration::from_millis(500)); From 05b8818de01866fa988a8f65a395690f02176b34 Mon Sep 17 00:00:00 2001 From: eZio Pan Date: Mon, 18 Dec 2023 01:02:58 +0800 Subject: [PATCH 3/6] typo fix --- examples/stm32f4/src/bin/ws2812_spi.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/stm32f4/src/bin/ws2812_spi.rs b/examples/stm32f4/src/bin/ws2812_spi.rs index 170f0b59b..eca657900 100644 --- a/examples/stm32f4/src/bin/ws2812_spi.rs +++ b/examples/stm32f4/src/bin/ws2812_spi.rs @@ -5,7 +5,7 @@ // Thus we can adjust SPI to send each *round* of data at 800 kHz, and in each *round*, we can adjust each *bit* to mimic 2 different PWM waveform. // such that the output waveform meet the bit representation of ws2812. // -// If you want to save SPI for other purpose, you may want to take a look at `ws2812_tim_dma.rs` file, which make use of TIM and DMA. +// If you want to save SPI for other purpose, you may want to take a look at `ws2812_pwm_dma.rs` file, which make use of TIM and DMA. // // Warning: // DO NOT stare at ws2812 directy (especially after each MCU Reset), its (max) brightness could easily make your eyes feel burn. @@ -20,11 +20,12 @@ use embassy_time::{Duration, Ticker, Timer}; use {defmt_rtt as _, panic_probe as _}; // we use 16 bit data frame format of SPI, to let timing as accurate as possible. -// thanks to loose tolerance of ws2812 timing, you can also use 8 bit data frame format, thus you need want to adjust the bit representation. +// thanks to loose tolerance of ws2812 timing, you can also use 8 bit data frame format, thus you will need to adjust the bit representation. const N0: u16 = 0b1111100000000000u16; // ws2812 Bit 0 high level timing const N1: u16 = 0b1111111111000000u16; // ws2812 Bit 1 high level timing -// ws2812 only need 24 bits for each LED, but we add one bit more to keep SPI output low +// ws2812 only need 24 bits for each LED, +// but we add one bit more to keep SPI output low at the end static TURN_OFF: [u16; 25] = [ N0, N0, N0, N0, N0, N0, N0, N0, // Green @@ -77,7 +78,7 @@ async fn main(_spawner: embassy_executor::Spawner) { let mut spi_config = spi::Config::default(); spi_config.frequency = khz(12_800); - // Since we only output waveform, then the Rx and Sck it is not considered + // Since we only output waveform, then the Rx and Sck and RxDma it is not considered let mut ws2812_spi = spi::Spi::new_txonly_nosck(dp.SPI1, dp.PB5, dp.DMA2_CH3, dma::NoDma, spi_config); // flip color at 2 Hz From 53fc344e4d46388cc742c139a263c1cdf0c16589 Mon Sep 17 00:00:00 2001 From: eZio Pan Date: Fri, 22 Dec 2023 01:24:31 +0800 Subject: [PATCH 4/6] fix timing, turn TIM UDE on only necessary, clean DMA FEIF after each Transfer --- examples/stm32f4/src/bin/ws2812_pwm_dma.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs index dc397eff1..c4181d024 100644 --- a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs +++ b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs @@ -64,9 +64,6 @@ async fn main(_spawner: Spawner) { CountingMode::EdgeAlignedUp, ); - // PAC level hacking, enable timer-update-event trigger DMA - pac::TIM3.dier().modify(|v| v.set_ude(true)); - // 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 @@ -104,13 +101,16 @@ async fn main(_spawner: Spawner) { dma_transfer_option.mburst = Burst::Incr8; // flip color at 2 Hz - let mut ticker = Ticker::every(Duration::from_micros(500)); + 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 @@ -121,6 +121,14 @@ async fn main(_spawner: Spawner) { 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; } From 2f75ffb2333904e1370a845ead88442c45b5ba5d Mon Sep 17 00:00:00 2001 From: eZio Pan Date: Fri, 22 Dec 2023 01:31:25 +0800 Subject: [PATCH 5/6] remove unused feature attribute --- examples/stm32f4/src/bin/ws2812_spi.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/stm32f4/src/bin/ws2812_spi.rs b/examples/stm32f4/src/bin/ws2812_spi.rs index eca657900..a280a3b77 100644 --- a/examples/stm32f4/src/bin/ws2812_spi.rs +++ b/examples/stm32f4/src/bin/ws2812_spi.rs @@ -12,7 +12,6 @@ #![no_std] #![no_main] -#![feature(type_alias_impl_trait)] use embassy_stm32::time::khz; use embassy_stm32::{dma, spi}; From dcd4e6384e3fab32809d01738ecd84011b4f0cc7 Mon Sep 17 00:00:00 2001 From: eZio Pan Date: Sat, 23 Dec 2023 19:45:56 +0800 Subject: [PATCH 6/6] enable output compare preload for TIM keep output waveform integrity --- examples/stm32f4/src/bin/ws2812_pwm_dma.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs index c4181d024..cdce36f2e 100644 --- a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs +++ b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs @@ -21,6 +21,7 @@ use embassy_executor::Spawner; use embassy_stm32::gpio::OutputType; use embassy_stm32::pac; +use embassy_stm32::pac::timer::vals::Ocpe; use embassy_stm32::time::khz; use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; use embassy_stm32::timer::{Channel, CountingMode}; @@ -89,6 +90,12 @@ async fn main(_spawner: Spawner) { 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, Ocpe::ENABLED)); + // make sure PWM output keep low on first start ws2812_pwm.set_duty(pwm_channel, 0);