diff options
| author | Dario Nieuwenhuis <[email protected]> | 2023-12-23 19:39:12 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-12-23 19:39:12 +0000 |
| commit | 55356795baef61e04bc7de5766f01e95be2dc930 (patch) | |
| tree | 2b2dfa6468d0553e3d24aee3f95461f0a4effebe /examples | |
| parent | 92758c3119739b80373428f0a640e08c1db7e27c (diff) | |
| parent | dcd4e6384e3fab32809d01738ecd84011b4f0cc7 (diff) | |
Merge pull request #2296 from eZioPan/stm32f4-example-ws2812-spi
(yet another) stm32f4 ws2812 example ...
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/stm32f4/src/bin/ws2812_pwm_dma.rs | 93 | ||||
| -rw-r--r-- | examples/stm32f4/src/bin/ws2812_spi.rs | 95 |
2 files changed, 152 insertions, 36 deletions
diff --git a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs index 9835c07e4..cdce36f2e 100644 --- a/examples/stm32f4/src/bin/ws2812_pwm_dma.rs +++ b/examples/stm32f4/src/bin/ws2812_pwm_dma.rs | |||
| @@ -1,7 +1,16 @@ | |||
| 1 | // Configure TIM3 in PWM mode, and start DMA Transfer(s) to send color data into ws2812. | 1 | // Configure TIM3 in PWM mode, and start DMA Transfer(s) to send color data into ws2812. |
| 2 | // We assume the DIN pin of ws2812 connect to GPIO PB4, and ws2812 is properly powered. | 2 | // We assume the DIN pin of ws2812 connect to GPIO PB4, and ws2812 is properly powered. |
| 3 | // | 3 | // |
| 4 | // This demo is a combination of HAL, PAC, and manually invoke `dma::Transfer` | 4 | // 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. |
| 5 | // Thus we can set TIM overflow at 800 kHz, and let TIM Update Event trigger a DMA transfer, then let DMA change CCR value, | ||
| 6 | // such that pwm duty ratio meet the bit representation of ws2812. | ||
| 7 | // | ||
| 8 | // You may want to modify TIM CCR with Cortex core directly, | ||
| 9 | // but according to my test, Cortex core will need to run far more than 100 MHz to catch up with TIM. | ||
| 10 | // Thus we need to use a DMA. | ||
| 11 | // | ||
| 12 | // This demo is a combination of HAL, PAC, and manually invoke `dma::Transfer`. | ||
| 13 | // 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. | ||
| 5 | // | 14 | // |
| 6 | // Warning: | 15 | // Warning: |
| 7 | // DO NOT stare at ws2812 directy (especially after each MCU Reset), its (max) brightness could easily make your eyes feel burn. | 16 | // DO NOT stare at ws2812 directy (especially after each MCU Reset), its (max) brightness could easily make your eyes feel burn. |
| @@ -12,10 +21,11 @@ | |||
| 12 | use embassy_executor::Spawner; | 21 | use embassy_executor::Spawner; |
| 13 | use embassy_stm32::gpio::OutputType; | 22 | use embassy_stm32::gpio::OutputType; |
| 14 | use embassy_stm32::pac; | 23 | use embassy_stm32::pac; |
| 24 | use embassy_stm32::pac::timer::vals::Ocpe; | ||
| 15 | use embassy_stm32::time::khz; | 25 | use embassy_stm32::time::khz; |
| 16 | use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; | 26 | use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; |
| 17 | use embassy_stm32::timer::{Channel, CountingMode}; | 27 | use embassy_stm32::timer::{Channel, CountingMode}; |
| 18 | use embassy_time::Timer; | 28 | use embassy_time::{Duration, Ticker, Timer}; |
| 19 | use {defmt_rtt as _, panic_probe as _}; | 29 | use {defmt_rtt as _, panic_probe as _}; |
| 20 | 30 | ||
| 21 | #[embassy_executor::main] | 31 | #[embassy_executor::main] |
| @@ -32,7 +42,6 @@ async fn main(_spawner: Spawner) { | |||
| 32 | freq: mhz(12), | 42 | freq: mhz(12), |
| 33 | mode: HseMode::Oscillator, | 43 | mode: HseMode::Oscillator, |
| 34 | }); | 44 | }); |
| 35 | device_config.rcc.sys = Sysclk::PLL1_P; | ||
| 36 | device_config.rcc.pll_src = PllSource::HSE; | 45 | device_config.rcc.pll_src = PllSource::HSE; |
| 37 | device_config.rcc.pll = Some(Pll { | 46 | device_config.rcc.pll = Some(Pll { |
| 38 | prediv: PllPreDiv::DIV6, | 47 | prediv: PllPreDiv::DIV6, |
| @@ -41,6 +50,7 @@ async fn main(_spawner: Spawner) { | |||
| 41 | divq: None, | 50 | divq: None, |
| 42 | divr: None, | 51 | divr: None, |
| 43 | }); | 52 | }); |
| 53 | device_config.rcc.sys = Sysclk::PLL1_P; | ||
| 44 | } | 54 | } |
| 45 | 55 | ||
| 46 | let mut dp = embassy_stm32::init(device_config); | 56 | let mut dp = embassy_stm32::init(device_config); |
| @@ -55,14 +65,8 @@ async fn main(_spawner: Spawner) { | |||
| 55 | CountingMode::EdgeAlignedUp, | 65 | CountingMode::EdgeAlignedUp, |
| 56 | ); | 66 | ); |
| 57 | 67 | ||
| 58 | // PAC level hacking, | ||
| 59 | // enable auto-reload preload, and enable timer-update-event trigger DMA | ||
| 60 | { | ||
| 61 | pac::TIM3.cr1().modify(|v| v.set_arpe(true)); | ||
| 62 | pac::TIM3.dier().modify(|v| v.set_ude(true)); | ||
| 63 | } | ||
| 64 | |||
| 65 | // construct ws2812 non-return-to-zero (NRZ) code bit by bit | 68 | // construct ws2812 non-return-to-zero (NRZ) code bit by bit |
| 69 | // ws2812 only need 24 bits for each LED, but we add one bit more to keep PWM output low | ||
| 66 | 70 | ||
| 67 | let max_duty = ws2812_pwm.get_max_duty(); | 71 | let max_duty = ws2812_pwm.get_max_duty(); |
| 68 | let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing | 72 | let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing |
| @@ -82,10 +86,16 @@ async fn main(_spawner: Spawner) { | |||
| 82 | 0, // keep PWM output low after a transfer | 86 | 0, // keep PWM output low after a transfer |
| 83 | ]; | 87 | ]; |
| 84 | 88 | ||
| 85 | let color_list = [&turn_off, &dim_white]; | 89 | let color_list = &[&turn_off, &dim_white]; |
| 86 | 90 | ||
| 87 | let pwm_channel = Channel::Ch1; | 91 | let pwm_channel = Channel::Ch1; |
| 88 | 92 | ||
| 93 | // PAC level hacking, enable output compare preload | ||
| 94 | // keep output waveform integrity | ||
| 95 | pac::TIM3 | ||
| 96 | .ccmr_output(pwm_channel.index()) | ||
| 97 | .modify(|v| v.set_ocpe(0, Ocpe::ENABLED)); | ||
| 98 | |||
| 89 | // make sure PWM output keep low on first start | 99 | // make sure PWM output keep low on first start |
| 90 | ws2812_pwm.set_duty(pwm_channel, 0); | 100 | ws2812_pwm.set_duty(pwm_channel, 0); |
| 91 | 101 | ||
| @@ -97,34 +107,45 @@ async fn main(_spawner: Spawner) { | |||
| 97 | dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full); | 107 | dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full); |
| 98 | dma_transfer_option.mburst = Burst::Incr8; | 108 | dma_transfer_option.mburst = Burst::Incr8; |
| 99 | 109 | ||
| 100 | let mut color_list_index = 0; | 110 | // flip color at 2 Hz |
| 111 | let mut ticker = Ticker::every(Duration::from_millis(500)); | ||
| 101 | 112 | ||
| 102 | loop { | 113 | loop { |
| 103 | // start PWM output | 114 | for &color in color_list { |
| 104 | ws2812_pwm.enable(pwm_channel); | 115 | // start PWM output |
| 105 | 116 | ws2812_pwm.enable(pwm_channel); | |
| 106 | unsafe { | 117 | |
| 107 | Transfer::new_write( | 118 | // PAC level hacking, enable timer-update-event trigger DMA |
| 108 | // with &mut, we can easily reuse same DMA channel multiple times | 119 | pac::TIM3.dier().modify(|v| v.set_ude(true)); |
| 109 | &mut dp.DMA1_CH2, | 120 | |
| 110 | 5, | 121 | unsafe { |
| 111 | color_list[color_list_index], | 122 | Transfer::new_write( |
| 112 | pac::TIM3.ccr(pwm_channel.index()).as_ptr() as *mut _, | 123 | // with &mut, we can easily reuse same DMA channel multiple times |
| 113 | dma_transfer_option, | 124 | &mut dp.DMA1_CH2, |
| 114 | ) | 125 | 5, |
| 115 | .await; | 126 | color, |
| 116 | // ws2812 need at least 50 us low level input to confirm the input data and change it's state | 127 | pac::TIM3.ccr(pwm_channel.index()).as_ptr() as *mut _, |
| 117 | Timer::after_micros(50).await; | 128 | dma_transfer_option, |
| 129 | ) | ||
| 130 | .await; | ||
| 131 | |||
| 132 | // Turn off timer-update-event trigger DMA as soon as possible. | ||
| 133 | // Then clean the FIFO Error Flag if set. | ||
| 134 | pac::TIM3.dier().modify(|v| v.set_ude(false)); | ||
| 135 | if pac::DMA1.isr(0).read().feif(2) { | ||
| 136 | pac::DMA1.ifcr(0).write(|v| v.set_feif(2, true)); | ||
| 137 | } | ||
| 138 | |||
| 139 | // ws2812 need at least 50 us low level input to confirm the input data and change it's state | ||
| 140 | Timer::after_micros(50).await; | ||
| 141 | } | ||
| 142 | |||
| 143 | // stop PWM output for saving some energy | ||
| 144 | ws2812_pwm.disable(pwm_channel); | ||
| 145 | |||
| 146 | // wait until ticker tick | ||
| 147 | ticker.next().await; | ||
| 118 | } | 148 | } |
| 119 | |||
| 120 | // stop PWM output for saving some energy | ||
| 121 | ws2812_pwm.disable(pwm_channel); | ||
| 122 | |||
| 123 | // wait another half second, so that we can see color change | ||
| 124 | Timer::after_millis(500).await; | ||
| 125 | |||
| 126 | // flip the index bit so that next round DMA transfer the other color data | ||
| 127 | color_list_index ^= 1; | ||
| 128 | } | 149 | } |
| 129 | } | 150 | } |
| 130 | } | 151 | } |
diff --git a/examples/stm32f4/src/bin/ws2812_spi.rs b/examples/stm32f4/src/bin/ws2812_spi.rs new file mode 100644 index 000000000..a280a3b77 --- /dev/null +++ b/examples/stm32f4/src/bin/ws2812_spi.rs | |||
| @@ -0,0 +1,95 @@ | |||
| 1 | // Mimic PWM with SPI, to control ws2812 | ||
| 2 | // We assume the DIN pin of ws2812 connect to GPIO PB5, and ws2812 is properly powered. | ||
| 3 | // | ||
| 4 | // 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. | ||
| 5 | // 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. | ||
| 6 | // such that the output waveform meet the bit representation of ws2812. | ||
| 7 | // | ||
| 8 | // 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. | ||
| 9 | // | ||
| 10 | // Warning: | ||
| 11 | // DO NOT stare at ws2812 directy (especially after each MCU Reset), its (max) brightness could easily make your eyes feel burn. | ||
| 12 | |||
| 13 | #![no_std] | ||
| 14 | #![no_main] | ||
| 15 | |||
| 16 | use embassy_stm32::time::khz; | ||
| 17 | use embassy_stm32::{dma, spi}; | ||
| 18 | use embassy_time::{Duration, Ticker, Timer}; | ||
| 19 | use {defmt_rtt as _, panic_probe as _}; | ||
| 20 | |||
| 21 | // we use 16 bit data frame format of SPI, to let timing as accurate as possible. | ||
| 22 | // 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. | ||
| 23 | const N0: u16 = 0b1111100000000000u16; // ws2812 Bit 0 high level timing | ||
| 24 | const N1: u16 = 0b1111111111000000u16; // ws2812 Bit 1 high level timing | ||
| 25 | |||
| 26 | // ws2812 only need 24 bits for each LED, | ||
| 27 | // but we add one bit more to keep SPI output low at the end | ||
| 28 | |||
| 29 | static TURN_OFF: [u16; 25] = [ | ||
| 30 | N0, N0, N0, N0, N0, N0, N0, N0, // Green | ||
| 31 | N0, N0, N0, N0, N0, N0, N0, N0, // Red | ||
| 32 | N0, N0, N0, N0, N0, N0, N0, N0, // Blue | ||
| 33 | 0, // keep SPI output low after last bit | ||
| 34 | ]; | ||
| 35 | |||
| 36 | static DIM_WHITE: [u16; 25] = [ | ||
| 37 | N0, N0, N0, N0, N0, N0, N1, N0, // Green | ||
| 38 | N0, N0, N0, N0, N0, N0, N1, N0, // Red | ||
| 39 | N0, N0, N0, N0, N0, N0, N1, N0, // Blue | ||
| 40 | 0, // keep SPI output low after last bit | ||
| 41 | ]; | ||
| 42 | |||
| 43 | static COLOR_LIST: &[&[u16]] = &[&TURN_OFF, &DIM_WHITE]; | ||
| 44 | |||
| 45 | #[embassy_executor::main] | ||
| 46 | async fn main(_spawner: embassy_executor::Spawner) { | ||
| 47 | let mut device_config = embassy_stm32::Config::default(); | ||
| 48 | |||
| 49 | // Since we use 16 bit SPI, and we need each round 800 kHz, | ||
| 50 | // thus SPI output speed should be 800 kHz * 16 = 12.8 MHz, and APB clock should be 2 * 12.8 MHz = 25.6 MHz. | ||
| 51 | // | ||
| 52 | // As for my setup, with 12 MHz HSE, I got 25.5 MHz SYSCLK, which is slightly slower, but it's ok for ws2812. | ||
| 53 | { | ||
| 54 | use embassy_stm32::rcc::{Hse, HseMode, Pll, PllMul, PllPDiv, PllPreDiv, PllSource, Sysclk}; | ||
| 55 | use embassy_stm32::time::mhz; | ||
| 56 | device_config.enable_debug_during_sleep = true; | ||
| 57 | device_config.rcc.hse = Some(Hse { | ||
| 58 | freq: mhz(12), | ||
| 59 | mode: HseMode::Oscillator, | ||
| 60 | }); | ||
| 61 | device_config.rcc.pll_src = PllSource::HSE; | ||
| 62 | device_config.rcc.pll = Some(Pll { | ||
| 63 | prediv: PllPreDiv::DIV6, | ||
| 64 | mul: PllMul::MUL102, | ||
| 65 | divp: Some(PllPDiv::DIV8), | ||
| 66 | divq: None, | ||
| 67 | divr: None, | ||
| 68 | }); | ||
| 69 | device_config.rcc.sys = Sysclk::PLL1_P; | ||
| 70 | } | ||
| 71 | |||
| 72 | let dp = embassy_stm32::init(device_config); | ||
| 73 | |||
| 74 | // Set SPI output speed. | ||
| 75 | // It's ok to blindly set frequency to 12800 kHz, the hal crate will take care of the SPI CR1 BR field. | ||
| 76 | // And in my case, the real bit rate will be 25.5 MHz / 2 = 12_750 kHz | ||
| 77 | let mut spi_config = spi::Config::default(); | ||
| 78 | spi_config.frequency = khz(12_800); | ||
| 79 | |||
| 80 | // Since we only output waveform, then the Rx and Sck and RxDma it is not considered | ||
| 81 | let mut ws2812_spi = spi::Spi::new_txonly_nosck(dp.SPI1, dp.PB5, dp.DMA2_CH3, dma::NoDma, spi_config); | ||
| 82 | |||
| 83 | // flip color at 2 Hz | ||
| 84 | let mut ticker = Ticker::every(Duration::from_millis(500)); | ||
| 85 | |||
| 86 | loop { | ||
| 87 | for &color in COLOR_LIST { | ||
| 88 | ws2812_spi.write(color).await.unwrap(); | ||
| 89 | // ws2812 need at least 50 us low level input to confirm the input data and change it's state | ||
| 90 | Timer::after_micros(50).await; | ||
| 91 | // wait until ticker tick | ||
| 92 | ticker.next().await; | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
