aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/stm32f4/src/bin/ws2812_pwm_dma.rs93
-rw-r--r--examples/stm32f4/src/bin/ws2812_spi.rs95
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 @@
12use embassy_executor::Spawner; 21use embassy_executor::Spawner;
13use embassy_stm32::gpio::OutputType; 22use embassy_stm32::gpio::OutputType;
14use embassy_stm32::pac; 23use embassy_stm32::pac;
24use embassy_stm32::pac::timer::vals::Ocpe;
15use embassy_stm32::time::khz; 25use embassy_stm32::time::khz;
16use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; 26use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm};
17use embassy_stm32::timer::{Channel, CountingMode}; 27use embassy_stm32::timer::{Channel, CountingMode};
18use embassy_time::Timer; 28use embassy_time::{Duration, Ticker, Timer};
19use {defmt_rtt as _, panic_probe as _}; 29use {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
16use embassy_stm32::time::khz;
17use embassy_stm32::{dma, spi};
18use embassy_time::{Duration, Ticker, Timer};
19use {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.
23const N0: u16 = 0b1111100000000000u16; // ws2812 Bit 0 high level timing
24const 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
29static 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
36static 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
43static COLOR_LIST: &[&[u16]] = &[&TURN_OFF, &DIM_WHITE];
44
45#[embassy_executor::main]
46async 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}