diff options
| author | eZio Pan <[email protected]> | 2023-12-17 23:47:00 +0800 |
|---|---|---|
| committer | eZio Pan <[email protected]> | 2023-12-17 23:47:00 +0800 |
| commit | 0a890cfbe7fc10bc40f2e97bc4fac17630e9864f (patch) | |
| tree | 58079b886a34c3878dc8d0f9528e23ed92f02e2c | |
| parent | a2d4bab2f8a4a9b994bc0289938a9f725950715f (diff) | |
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.
| -rw-r--r-- | examples/stm32f4/src/bin/ws2812_pwm_dma.rs | 76 | ||||
| -rw-r--r-- | examples/stm32f4/src/bin/ws2812_spi.rs | 95 |
2 files changed, 136 insertions, 35 deletions
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 @@ | |||
| 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. |
| @@ -16,7 +25,7 @@ use embassy_stm32::pac; | |||
| 16 | use embassy_stm32::time::khz; | 25 | use embassy_stm32::time::khz; |
| 17 | use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; | 26 | use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; |
| 18 | use embassy_stm32::timer::{Channel, CountingMode}; | 27 | use embassy_stm32::timer::{Channel, CountingMode}; |
| 19 | use embassy_time::Timer; | 28 | use embassy_time::{Duration, Ticker, Timer}; |
| 20 | use {defmt_rtt as _, panic_probe as _}; | 29 | use {defmt_rtt as _, panic_probe as _}; |
| 21 | 30 | ||
| 22 | #[embassy_executor::main] | 31 | #[embassy_executor::main] |
| @@ -33,7 +42,6 @@ async fn main(_spawner: Spawner) { | |||
| 33 | freq: mhz(12), | 42 | freq: mhz(12), |
| 34 | mode: HseMode::Oscillator, | 43 | mode: HseMode::Oscillator, |
| 35 | }); | 44 | }); |
| 36 | device_config.rcc.sys = Sysclk::PLL1_P; | ||
| 37 | device_config.rcc.pll_src = PllSource::HSE; | 45 | device_config.rcc.pll_src = PllSource::HSE; |
| 38 | device_config.rcc.pll = Some(Pll { | 46 | device_config.rcc.pll = Some(Pll { |
| 39 | prediv: PllPreDiv::DIV6, | 47 | prediv: PllPreDiv::DIV6, |
| @@ -42,6 +50,7 @@ async fn main(_spawner: Spawner) { | |||
| 42 | divq: None, | 50 | divq: None, |
| 43 | divr: None, | 51 | divr: None, |
| 44 | }); | 52 | }); |
| 53 | device_config.rcc.sys = Sysclk::PLL1_P; | ||
| 45 | } | 54 | } |
| 46 | 55 | ||
| 47 | let mut dp = embassy_stm32::init(device_config); | 56 | let mut dp = embassy_stm32::init(device_config); |
| @@ -56,14 +65,11 @@ async fn main(_spawner: Spawner) { | |||
| 56 | CountingMode::EdgeAlignedUp, | 65 | CountingMode::EdgeAlignedUp, |
| 57 | ); | 66 | ); |
| 58 | 67 | ||
| 59 | // PAC level hacking, | 68 | // PAC level hacking, enable timer-update-event trigger DMA |
| 60 | // enable auto-reload preload, and enable timer-update-event trigger DMA | 69 | pac::TIM3.dier().modify(|v| v.set_ude(true)); |
| 61 | { | ||
| 62 | pac::TIM3.cr1().modify(|v| v.set_arpe(true)); | ||
| 63 | pac::TIM3.dier().modify(|v| v.set_ude(true)); | ||
| 64 | } | ||
| 65 | 70 | ||
| 66 | // construct ws2812 non-return-to-zero (NRZ) code bit by bit | 71 | // construct ws2812 non-return-to-zero (NRZ) code bit by bit |
| 72 | // ws2812 only need 24 bits for each LED, but we add one bit more to keep PWM output low | ||
| 67 | 73 | ||
| 68 | let max_duty = ws2812_pwm.get_max_duty(); | 74 | let max_duty = ws2812_pwm.get_max_duty(); |
| 69 | let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing | 75 | let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing |
| @@ -83,7 +89,7 @@ async fn main(_spawner: Spawner) { | |||
| 83 | 0, // keep PWM output low after a transfer | 89 | 0, // keep PWM output low after a transfer |
| 84 | ]; | 90 | ]; |
| 85 | 91 | ||
| 86 | let color_list = [&turn_off, &dim_white]; | 92 | let color_list = &[&turn_off, &dim_white]; |
| 87 | 93 | ||
| 88 | let pwm_channel = Channel::Ch1; | 94 | let pwm_channel = Channel::Ch1; |
| 89 | 95 | ||
| @@ -98,34 +104,34 @@ async fn main(_spawner: Spawner) { | |||
| 98 | dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full); | 104 | dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full); |
| 99 | dma_transfer_option.mburst = Burst::Incr8; | 105 | dma_transfer_option.mburst = Burst::Incr8; |
| 100 | 106 | ||
| 101 | let mut color_list_index = 0; | 107 | // flip color at 2 Hz |
| 108 | let mut ticker = Ticker::every(Duration::from_micros(500)); | ||
| 102 | 109 | ||
| 103 | loop { | 110 | loop { |
| 104 | // start PWM output | 111 | for &color in color_list { |
| 105 | ws2812_pwm.enable(pwm_channel); | 112 | // start PWM output |
| 106 | 113 | ws2812_pwm.enable(pwm_channel); | |
| 107 | unsafe { | 114 | |
| 108 | Transfer::new_write( | 115 | unsafe { |
| 109 | // with &mut, we can easily reuse same DMA channel multiple times | 116 | Transfer::new_write( |
| 110 | &mut dp.DMA1_CH2, | 117 | // with &mut, we can easily reuse same DMA channel multiple times |
| 111 | 5, | 118 | &mut dp.DMA1_CH2, |
| 112 | color_list[color_list_index], | 119 | 5, |
| 113 | pac::TIM3.ccr(pwm_channel.raw()).as_ptr() as *mut _, | 120 | color, |
| 114 | dma_transfer_option, | 121 | pac::TIM3.ccr(pwm_channel.raw()).as_ptr() as *mut _, |
| 115 | ) | 122 | dma_transfer_option, |
| 116 | .await; | 123 | ) |
| 117 | // ws2812 need at least 50 us low level input to confirm the input data and change it's state | 124 | .await; |
| 118 | Timer::after_micros(50).await; | 125 | // ws2812 need at least 50 us low level input to confirm the input data and change it's state |
| 126 | Timer::after_micros(50).await; | ||
| 127 | } | ||
| 128 | |||
| 129 | // stop PWM output for saving some energy | ||
| 130 | ws2812_pwm.disable(pwm_channel); | ||
| 131 | |||
| 132 | // wait until ticker tick | ||
| 133 | ticker.next().await; | ||
| 119 | } | 134 | } |
| 120 | |||
| 121 | // stop PWM output for saving some energy | ||
| 122 | ws2812_pwm.disable(pwm_channel); | ||
| 123 | |||
| 124 | // wait another half second, so that we can see color change | ||
| 125 | Timer::after_millis(500).await; | ||
| 126 | |||
| 127 | // flip the index bit so that next round DMA transfer the other color data | ||
| 128 | color_list_index ^= 1; | ||
| 129 | } | 135 | } |
| 130 | } | 136 | } |
| 131 | } | 137 | } |
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 @@ | |||
| 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_tim_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 | #![feature(type_alias_impl_trait)] | ||
| 16 | |||
| 17 | use embassy_stm32::time::khz; | ||
| 18 | use embassy_stm32::{dma, spi}; | ||
| 19 | use embassy_time::{Duration, Ticker, Timer}; | ||
| 20 | use {defmt_rtt as _, panic_probe as _}; | ||
| 21 | |||
| 22 | // we use 16 bit data frame format of SPI, to let timing as accurate as possible. | ||
| 23 | // 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. | ||
| 24 | const N0: u16 = 0b1111100000000000u16; // ws2812 Bit 0 high level timing | ||
| 25 | const N1: u16 = 0b1111111111000000u16; // ws2812 Bit 1 high level timing | ||
| 26 | |||
| 27 | // ws2812 only need 24 bits for each LED, but we add one bit more to keep SPI output low | ||
| 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 it is not considered | ||
| 81 | let mut ws2812_spi = spi::Spi::new_txonly_nosck(dp.SPI1, dp.PB5, dp.DMA2_CH2, 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 | } | ||
