Merge pull request #2296 from eZioPan/stm32f4-example-ws2812-spi
(yet another) stm32f4 ws2812 example ...
This commit is contained in:
commit
55356795ba
2 changed files with 151 additions and 35 deletions
examples/stm32f4/src/bin
|
@ -1,7 +1,16 @@
|
||||||
// Configure TIM3 in PWM mode, and start DMA Transfer(s) to send color data into ws2812.
|
// 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.
|
// 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:
|
// Warning:
|
||||||
// DO NOT stare at ws2812 directy (especially after each MCU Reset), its (max) brightness could easily make your eyes feel burn.
|
// 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 @@
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_stm32::gpio::OutputType;
|
use embassy_stm32::gpio::OutputType;
|
||||||
use embassy_stm32::pac;
|
use embassy_stm32::pac;
|
||||||
|
use embassy_stm32::pac::timer::vals::Ocpe;
|
||||||
use embassy_stm32::time::khz;
|
use embassy_stm32::time::khz;
|
||||||
use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm};
|
use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm};
|
||||||
use embassy_stm32::timer::{Channel, CountingMode};
|
use embassy_stm32::timer::{Channel, CountingMode};
|
||||||
use embassy_time::Timer;
|
use embassy_time::{Duration, Ticker, Timer};
|
||||||
use {defmt_rtt as _, panic_probe as _};
|
use {defmt_rtt as _, panic_probe as _};
|
||||||
|
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
|
@ -32,7 +42,6 @@ async fn main(_spawner: Spawner) {
|
||||||
freq: mhz(12),
|
freq: mhz(12),
|
||||||
mode: HseMode::Oscillator,
|
mode: HseMode::Oscillator,
|
||||||
});
|
});
|
||||||
device_config.rcc.sys = Sysclk::PLL1_P;
|
|
||||||
device_config.rcc.pll_src = PllSource::HSE;
|
device_config.rcc.pll_src = PllSource::HSE;
|
||||||
device_config.rcc.pll = Some(Pll {
|
device_config.rcc.pll = Some(Pll {
|
||||||
prediv: PllPreDiv::DIV6,
|
prediv: PllPreDiv::DIV6,
|
||||||
|
@ -41,6 +50,7 @@ async fn main(_spawner: Spawner) {
|
||||||
divq: None,
|
divq: None,
|
||||||
divr: None,
|
divr: None,
|
||||||
});
|
});
|
||||||
|
device_config.rcc.sys = Sysclk::PLL1_P;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut dp = embassy_stm32::init(device_config);
|
let mut dp = embassy_stm32::init(device_config);
|
||||||
|
@ -55,14 +65,8 @@ async fn main(_spawner: Spawner) {
|
||||||
CountingMode::EdgeAlignedUp,
|
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct ws2812 non-return-to-zero (NRZ) code bit by bit
|
// 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 max_duty = ws2812_pwm.get_max_duty();
|
||||||
let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing
|
let n0 = 8 * max_duty / 25; // ws2812 Bit 0 high level timing
|
||||||
|
@ -82,10 +86,16 @@ async fn main(_spawner: Spawner) {
|
||||||
0, // keep PWM output low after a transfer
|
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;
|
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
|
// make sure PWM output keep low on first start
|
||||||
ws2812_pwm.set_duty(pwm_channel, 0);
|
ws2812_pwm.set_duty(pwm_channel, 0);
|
||||||
|
|
||||||
|
@ -97,34 +107,45 @@ async fn main(_spawner: Spawner) {
|
||||||
dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full);
|
dma_transfer_option.fifo_threshold = Some(FifoThreshold::Full);
|
||||||
dma_transfer_option.mburst = Burst::Incr8;
|
dma_transfer_option.mburst = Burst::Incr8;
|
||||||
|
|
||||||
let mut color_list_index = 0;
|
// flip color at 2 Hz
|
||||||
|
let mut ticker = Ticker::every(Duration::from_millis(500));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// start PWM output
|
for &color in color_list {
|
||||||
ws2812_pwm.enable(pwm_channel);
|
// start PWM output
|
||||||
|
ws2812_pwm.enable(pwm_channel);
|
||||||
|
|
||||||
unsafe {
|
// PAC level hacking, enable timer-update-event trigger DMA
|
||||||
Transfer::new_write(
|
pac::TIM3.dier().modify(|v| v.set_ude(true));
|
||||||
// with &mut, we can easily reuse same DMA channel multiple times
|
|
||||||
&mut dp.DMA1_CH2,
|
unsafe {
|
||||||
5,
|
Transfer::new_write(
|
||||||
color_list[color_list_index],
|
// with &mut, we can easily reuse same DMA channel multiple times
|
||||||
pac::TIM3.ccr(pwm_channel.index()).as_ptr() as *mut _,
|
&mut dp.DMA1_CH2,
|
||||||
dma_transfer_option,
|
5,
|
||||||
)
|
color,
|
||||||
.await;
|
pac::TIM3.ccr(pwm_channel.index()).as_ptr() as *mut _,
|
||||||
// ws2812 need at least 50 us low level input to confirm the input data and change it's state
|
dma_transfer_option,
|
||||||
Timer::after_micros(50).await;
|
)
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
95
examples/stm32f4/src/bin/ws2812_spi.rs
Normal file
95
examples/stm32f4/src/bin/ws2812_spi.rs
Normal file
|
@ -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_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.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
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 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 at the end
|
||||||
|
|
||||||
|
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 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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue