From 3a958cdf685a5d4780572c310aed26272839f78a Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Wed, 19 Nov 2025 13:50:20 +0100 Subject: feat: Add generation of a continuous waveforms for SimplePWM --- embassy-stm32/src/timer/low_level.rs | 44 +++++++++++++++++++++++++++++++++-- embassy-stm32/src/timer/simple_pwm.rs | 6 +++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/embassy-stm32/src/timer/low_level.rs b/embassy-stm32/src/timer/low_level.rs index f0105ece8..670298d23 100644 --- a/embassy-stm32/src/timer/low_level.rs +++ b/embassy-stm32/src/timer/low_level.rs @@ -673,7 +673,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { self.enable_update_dma(true); } - self.waveform_helper(dma, req, channel, duty).await; + self.waveform_helper(dma, req, channel, duty, false).await; // Since DMA is closed before timer update event trigger DMA is turn off, // this can almost always trigger a DMA FIFO error. @@ -780,6 +780,44 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { let cc_channel = C::CHANNEL; + let original_cc_dma_on_update = self.get_cc_dma_selection() == Ccds::ON_UPDATE; + let original_cc_dma_enabled = self.get_c fsc_dma_enable_state(cc_channel); + + // redirect CC DMA request onto Update Event + if !original_cc_dma_on_update { + self.set_cc_dma_selection(Ccds::ON_UPDATE) + } + + if !original_cc_dma_enabled { + self.set_cc_dma_enable_state(cc_channel, true); + } + + self.waveform_helper(dma, req, cc_channel, duty, false).await; + + // Since DMA is closed before timer Capture Compare Event trigger DMA is turn off, + // this can almost always trigger a DMA FIFO error. + // + // optional TODO: + // clean FEIF after disable UDE + if !original_cc_dma_enabled { + self.set_cc_dma_enable_state(cc_channel, false); + } + + if !original_cc_dma_on_update { + self.set_cc_dma_selection(Ccds::ON_COMPARE) + } + } + + /// Generate a sequence of PWM waveform that will run continously + /// You may want to start this in a new thread as this will block forever + pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { + use crate::pac::timer::vals::Ccds; + + #[allow(clippy::let_unit_value)] // eg. stm32f334 + let req = dma.request(); + + let cc_channel = C::CHANNEL; + let original_cc_dma_on_update = self.get_cc_dma_selection() == Ccds::ON_UPDATE; let original_cc_dma_enabled = self.get_cc_dma_enable_state(cc_channel); @@ -792,7 +830,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { self.set_cc_dma_enable_state(cc_channel, true); } - self.waveform_helper(dma, req, cc_channel, duty).await; + self.waveform_helper(dma, req, cc_channel, duty, true).await; // Since DMA is closed before timer Capture Compare Event trigger DMA is turn off, // this can almost always trigger a DMA FIFO error. @@ -814,6 +852,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { req: dma::Request, channel: Channel, duty: &[u16], + circular: bool, ) { let original_duty_state = self.get_compare_value(channel); let original_enable_state = self.get_channel_enable_state(channel); @@ -832,6 +871,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { fifo_threshold: Some(FifoThreshold::Full), #[cfg(not(any(bdma, gpdma)))] mburst: Burst::Incr8, + circular: circular, ..Default::default() }; diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index 6c9ef17e0..56d00ea59 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -368,6 +368,12 @@ impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { pub async fn waveform(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { self.inner.waveform(dma, duty).await; } + /// Generate a sequence of PWM waveform that will run continously + /// You may want to start this in a new thread as this will block forever + #[inline(always)] + pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { + self.inner.waveform_continuous(dma, duty).await; + } } impl<'d, T: GeneralInstance4Channel> embedded_hal_1::pwm::ErrorType for SimplePwmChannel<'d, T> { -- cgit From ac1f0ec4ecc6450de7d1c6044e799bca8791f7e5 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Wed, 19 Nov 2025 14:00:02 +0100 Subject: doc: Add changelog --- embassy-stm32/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/embassy-stm32/CHANGELOG.md b/embassy-stm32/CHANGELOG.md index b6caf8f65..898a91e4b 100644 --- a/embassy-stm32/CHANGELOG.md +++ b/embassy-stm32/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - ReleaseDate +- feat: Add continuous waveform method to SimplePWM - change: stm32/eth: ethernet no longer has a hard dependency on station management, and station management can be used independently ([#4871](https://github.com/embassy-rs/embassy/pull/4871)) - feat: allow embassy_executor::main for low power - feat: Add waveform methods to ComplementaryPwm -- cgit From 36f4075b9054576ccf6dba2dedb08c62484a0599 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Sun, 23 Nov 2025 12:29:56 +0100 Subject: fix: Fix waveform for channels > 1 and disallow for unsupported dmas --- embassy-stm32/src/timer/low_level.rs | 12 +++++-- examples/stm32f7/src/bin/pwm.rs | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 examples/stm32f7/src/bin/pwm.rs diff --git a/embassy-stm32/src/timer/low_level.rs b/embassy-stm32/src/timer/low_level.rs index 670298d23..307d614bf 100644 --- a/embassy-stm32/src/timer/low_level.rs +++ b/embassy-stm32/src/timer/low_level.rs @@ -781,7 +781,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { let cc_channel = C::CHANNEL; let original_cc_dma_on_update = self.get_cc_dma_selection() == Ccds::ON_UPDATE; - let original_cc_dma_enabled = self.get_c fsc_dma_enable_state(cc_channel); + let original_cc_dma_enabled = self.get_cc_dma_enable_state(cc_channel); // redirect CC DMA request onto Update Event if !original_cc_dma_on_update { @@ -810,7 +810,12 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { /// Generate a sequence of PWM waveform that will run continously /// You may want to start this in a new thread as this will block forever + pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { + + #[cfg(any(bdma, gpdma))] + panic!("unsupported DMA"); + use crate::pac::timer::vals::Ccds; #[allow(clippy::let_unit_value)] // eg. stm32f334 @@ -871,6 +876,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { fifo_threshold: Some(FifoThreshold::Full), #[cfg(not(any(bdma, gpdma)))] mburst: Burst::Incr8, + #[cfg(not(any(bdma, gpdma)))] circular: circular, ..Default::default() }; @@ -881,7 +887,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { dma, req, duty, - self.regs_1ch().ccr(channel.index()).as_ptr() as *mut u16, + self.regs_gp16().ccr(channel.index()).as_ptr() as *mut u16, dma_transfer_option, ) .await @@ -896,7 +902,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { dma, req, duty, - self.regs_1ch().ccr(channel.index()).as_ptr() as *mut u32, + self.regs_gp16().ccr(channel.index()).as_ptr() as *mut u32, dma_transfer_option, ) .await diff --git a/examples/stm32f7/src/bin/pwm.rs b/examples/stm32f7/src/bin/pwm.rs new file mode 100644 index 000000000..872d99859 --- /dev/null +++ b/examples/stm32f7/src/bin/pwm.rs @@ -0,0 +1,65 @@ +#![no_std] +#![no_main] + +use defmt::{panic, *}; +use embassy_executor::Spawner; +use embassy_futures::join::join; +use embassy_stm32::time::Hertz; +use embassy_stm32::{Config, peripherals}; +use embassy_stm32::gpio::OutputType; +use embassy_stm32::time::mhz; +use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +// If you are trying this and your USB device doesn't connect, the most +// common issues are the RCC config and vbus_detection +// +// See https://embassy.dev/book/#_the_usb_examples_are_not_working_on_my_board_is_there_anything_else_i_need_to_configure +// for more information. +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + info!("Hello World!"); + + let mut config = Config::default(); + { + use embassy_stm32::rcc::*; + config.rcc.hse = Some(Hse { + freq: Hertz(8_000_000), + mode: HseMode::Bypass, + }); + config.rcc.pll_src = PllSource::HSE; + config.rcc.pll = Some(Pll { + prediv: PllPreDiv::DIV4, + mul: PllMul::MUL200, + divp: Some(PllPDiv::DIV2), // 8mhz / 4 * 200 / 2 = 200Mhz + divq: Some(PllQDiv::DIV4), // 8mhz / 4 * 200 / 4 = 100Mhz + divr: None, + }); + config.rcc.ahb_pre = AHBPrescaler::DIV1; + config.rcc.apb1_pre = APBPrescaler::DIV4; + config.rcc.apb2_pre = APBPrescaler::DIV2; + config.rcc.sys = Sysclk::PLL1_P; + } + let p = embassy_stm32::init(config); + let ch1_pin = PwmPin::new(p.PE9, OutputType::PushPull); + let ch2_pin = PwmPin::new(p.PE11, OutputType::PushPull); + let mut pwm = SimplePwm::new(p.TIM1, Some(ch1_pin), Some(ch2_pin), None, None, mhz(1), Default::default()); + let mut ch1 = pwm.ch1(); + ch1.enable(); + info!("PWM initialized"); + info!("PWM max duty {}", ch1.max_duty_cycle()); + + info!("PWM duty on channel 1 (D6) 50%"); + ch1.set_duty_cycle_fraction(1, 2); + info!("PWM waveform on channel 2 (D5)"); + const max_duty: usize = 200; + let mut duty = [0u16;max_duty]; + for i in 0..max_duty { + duty[i] = i as u16; + } + pwm.waveform_continuous::(p.DMA2_CH6, &duty).await; + + +} + -- cgit From 1a79bb51bf1490de5cc6f6ad021edd161c088b9f Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Sun, 23 Nov 2025 23:38:38 +0100 Subject: wip: adding basic ringbuffered structure --- embassy-stm32/src/timer/mod.rs | 1 + embassy-stm32/src/timer/ringbuffered.rs | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 embassy-stm32/src/timer/ringbuffered.rs diff --git a/embassy-stm32/src/timer/mod.rs b/embassy-stm32/src/timer/mod.rs index 804d1ef37..aef3598f1 100644 --- a/embassy-stm32/src/timer/mod.rs +++ b/embassy-stm32/src/timer/mod.rs @@ -13,6 +13,7 @@ pub mod one_pulse; pub mod pwm_input; pub mod qei; pub mod simple_pwm; +pub mod ringbuffered; use crate::interrupt; use crate::rcc::RccPeripheral; diff --git a/embassy-stm32/src/timer/ringbuffered.rs b/embassy-stm32/src/timer/ringbuffered.rs new file mode 100644 index 000000000..d20c5d532 --- /dev/null +++ b/embassy-stm32/src/timer/ringbuffered.rs @@ -0,0 +1,47 @@ +//! RingBuffered PWM driver. + +use core::mem::ManuallyDrop; + +use super::low_level::Timer; +use super::{Channel, GeneralInstance4Channel, TimerChannel, TimerPin}; +use crate::Peri; +use crate::dma::ringbuffer::WritableDmaRingBuffer; +use super::simple_pwm::SimplePwm; + +pub struct RingBufferedPwmChannel<'d, T: GeneralInstance4Channel> { + timer: ManuallyDrop>, + ring_buf: WritableDmaRingBuffer<'d, u8>, + channel: Channel, +} + +/// A group of four [`SimplePwmChannel`]s, obtained from [`SimplePwm::split`]. +pub struct RingBufferedPwmChannels<'d, T: GeneralInstance4Channel> { + /// Channel 1 + pub ch1: RingBufferedPwmChannel<'d, T>, + /// Channel 2 + pub ch2: RingBufferedPwmChannel<'d, T>, + /// Channel 3 + pub ch3: RingBufferedPwmChannel<'d, T>, + /// Channel 4 + pub ch4: RingBufferedPwmChannel<'d, T>, +} + +/// Simple PWM driver. +pub struct RingBufferedPwm<'d, T: GeneralInstance4Channel> { + inner: Timer<'d, T>, +} + +impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { + pub fn into_ring_buffered_channel(mut self, tx_dma: Peri<'_, impl super::Dma>, dma_buf: &'d mut [u8]) -> RingBufferedPwmChannel<'d> { + assert!(!dma_buf.is_empty() && dma_buf.len() <= 0xFFFF); + let ring_buf = WritableDmaRingBuffer::new(dma_buf); + let channel = C::CHANNEL; + RingBufferedPwmChannel { + timer: unsafe { self.inner.clone_unchecked() }, + channel, + ring_buf + } + + // let ring_buf = WriteableRingBuffer::new(); + } +} -- cgit From a227e61137a689ecd875c41a7efb5f2a6bb73876 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Sun, 23 Nov 2025 23:39:09 +0100 Subject: fix: Formatting and clippy --- examples/stm32f7/src/bin/pwm.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/examples/stm32f7/src/bin/pwm.rs b/examples/stm32f7/src/bin/pwm.rs index 872d99859..53efc2d8a 100644 --- a/examples/stm32f7/src/bin/pwm.rs +++ b/examples/stm32f7/src/bin/pwm.rs @@ -1,15 +1,13 @@ #![no_std] #![no_main] -use defmt::{panic, *}; +use defmt::*; use embassy_executor::Spawner; -use embassy_futures::join::join; -use embassy_stm32::time::Hertz; -use embassy_stm32::{Config, peripherals}; +use embassy_stm32::Config; use embassy_stm32::gpio::OutputType; +use embassy_stm32::time::Hertz; use embassy_stm32::time::mhz; use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; -use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; // If you are trying this and your USB device doesn't connect, the most @@ -44,7 +42,15 @@ async fn main(_spawner: Spawner) { let p = embassy_stm32::init(config); let ch1_pin = PwmPin::new(p.PE9, OutputType::PushPull); let ch2_pin = PwmPin::new(p.PE11, OutputType::PushPull); - let mut pwm = SimplePwm::new(p.TIM1, Some(ch1_pin), Some(ch2_pin), None, None, mhz(1), Default::default()); + let mut pwm = SimplePwm::new( + p.TIM1, + Some(ch1_pin), + Some(ch2_pin), + None, + None, + mhz(1), + Default::default(), + ); let mut ch1 = pwm.ch1(); ch1.enable(); info!("PWM initialized"); @@ -53,13 +59,11 @@ async fn main(_spawner: Spawner) { info!("PWM duty on channel 1 (D6) 50%"); ch1.set_duty_cycle_fraction(1, 2); info!("PWM waveform on channel 2 (D5)"); - const max_duty: usize = 200; - let mut duty = [0u16;max_duty]; - for i in 0..max_duty { + const MAX_DUTY: usize = 200; + let mut duty = [0u16; MAX_DUTY]; + for i in 0..MAX_DUTY { duty[i] = i as u16; } - pwm.waveform_continuous::(p.DMA2_CH6, &duty).await; - - + pwm.waveform_continuous::(p.DMA2_CH6, &duty) + .await; } - -- cgit From d45376583386272bc49fecc7eed8951067f84ac8 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Mon, 24 Nov 2025 21:42:54 +0100 Subject: feat: Implement basic ring buffered PWM channel --- embassy-stm32/src/timer/low_level.rs | 2 - embassy-stm32/src/timer/ringbuffered.rs | 151 +++++++++++++++++++++++++++----- embassy-stm32/src/timer/simple_pwm.rs | 35 ++++++++ 3 files changed, 163 insertions(+), 25 deletions(-) diff --git a/embassy-stm32/src/timer/low_level.rs b/embassy-stm32/src/timer/low_level.rs index 307d614bf..2cee5f1f5 100644 --- a/embassy-stm32/src/timer/low_level.rs +++ b/embassy-stm32/src/timer/low_level.rs @@ -813,8 +813,6 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { - #[cfg(any(bdma, gpdma))] - panic!("unsupported DMA"); use crate::pac::timer::vals::Ccds; diff --git a/embassy-stm32/src/timer/ringbuffered.rs b/embassy-stm32/src/timer/ringbuffered.rs index d20c5d532..bb602f8a7 100644 --- a/embassy-stm32/src/timer/ringbuffered.rs +++ b/embassy-stm32/src/timer/ringbuffered.rs @@ -1,19 +1,142 @@ //! RingBuffered PWM driver. use core::mem::ManuallyDrop; +use core::task::Waker; use super::low_level::Timer; -use super::{Channel, GeneralInstance4Channel, TimerChannel, TimerPin}; -use crate::Peri; -use crate::dma::ringbuffer::WritableDmaRingBuffer; -use super::simple_pwm::SimplePwm; +use super::{Channel, GeneralInstance4Channel}; +use crate::dma::ringbuffer::Error; +use crate::dma::WritableRingBuffer; pub struct RingBufferedPwmChannel<'d, T: GeneralInstance4Channel> { timer: ManuallyDrop>, - ring_buf: WritableDmaRingBuffer<'d, u8>, + ring_buf: WritableRingBuffer<'d, u16>, channel: Channel, } +impl<'d, T: GeneralInstance4Channel> RingBufferedPwmChannel<'d, T> { + pub(crate) fn new(timer: ManuallyDrop>, channel: Channel, ring_buf: WritableRingBuffer<'d, u16>) -> Self { + Self { + timer, + ring_buf, + channel, + } + } + + /// Start the ring buffer operation. + /// + /// You must call this after creating it for it to work. + pub fn start(&mut self) { + self.ring_buf.start() + } + + /// Clear all data in the ring buffer. + pub fn clear(&mut self) { + self.ring_buf.clear() + } + + /// Write elements directly to the raw buffer. This can be used to fill the buffer before starting the DMA transfer. + pub fn write_immediate(&mut self, buf: &[u16]) -> Result<(usize, usize), Error> { + self.ring_buf.write_immediate(buf) + } + + /// Write elements from the ring buffer + /// Return a tuple of the length written and the length remaining in the buffer + pub fn write(&mut self, buf: &[u16]) -> Result<(usize, usize), Error> { + self.ring_buf.write(buf) + } + + /// Write an exact number of elements to the ringbuffer. + pub async fn write_exact(&mut self, buffer: &[u16]) -> Result { + self.ring_buf.write_exact(buffer).await + } + + /// Wait for any ring buffer write error. + pub async fn wait_write_error(&mut self) -> Result { + self.ring_buf.wait_write_error().await + } + + /// The current length of the ringbuffer + pub fn len(&mut self) -> Result { + self.ring_buf.len() + } + + /// The capacity of the ringbuffer + pub const fn capacity(&self) -> usize { + self.ring_buf.capacity() + } + + /// Set a waker to be woken when at least one byte is send. + pub fn set_waker(&mut self, waker: &Waker) { + self.ring_buf.set_waker(waker) + } + + /// Request the DMA to reset. The configuration for this channel will not be preserved. + /// + /// This doesn't immediately stop the transfer, you have to wait until is_running returns false. + pub fn request_reset(&mut self) { + self.ring_buf.request_reset() + } + + /// Request the transfer to pause, keeping the existing configuration for this channel. + /// To restart the transfer, call [`start`](Self::start) again. + /// + /// This doesn't immediately stop the transfer, you have to wait until is_running returns false. + pub fn request_pause(&mut self) { + self.ring_buf.request_pause() + } + + /// Return whether DMA is still running. + /// + /// If this returns false, it can be because either the transfer finished, or it was requested to stop early with request_stop. + pub fn is_running(&mut self) -> bool { + self.ring_buf.is_running() + } + + /// Stop the DMA transfer and await until the buffer is empty. + /// + /// This disables the DMA transfer's circular mode so that the transfer stops when all available data has been written. + /// + /// This is designed to be used with streaming output data such as the I2S/SAI or DAC. + pub async fn stop(&mut self) { + self.ring_buf.stop().await + } + + /// Enable the given channel. + pub fn enable(&mut self) { + self.timer.enable_channel(self.channel, true); + } + + /// Disable the given channel. + pub fn disable(&mut self) { + self.timer.enable_channel(self.channel, false); + } + + /// Check whether given channel is enabled + pub fn is_enabled(&self) -> bool { + self.timer.get_channel_enable_state(self.channel) + } + + /// Get max duty value. + /// + /// This value depends on the configured frequency and the timer's clock rate from RCC. + pub fn max_duty_cycle(&self) -> u16 { + let max = self.timer.get_max_compare_value(); + assert!(max < u16::MAX as u32); + max as u16 + 1 + } + + /// Set the output polarity for a given channel. + pub fn set_polarity(&mut self, polarity: super::low_level::OutputPolarity) { + self.timer.set_output_polarity(self.channel, polarity); + } + + /// Set the output compare mode for a given channel. + pub fn set_output_compare_mode(&mut self, mode: super::low_level::OutputCompareMode) { + self.timer.set_output_compare_mode(self.channel, mode); + } +} + /// A group of four [`SimplePwmChannel`]s, obtained from [`SimplePwm::split`]. pub struct RingBufferedPwmChannels<'d, T: GeneralInstance4Channel> { /// Channel 1 @@ -26,22 +149,4 @@ pub struct RingBufferedPwmChannels<'d, T: GeneralInstance4Channel> { pub ch4: RingBufferedPwmChannel<'d, T>, } -/// Simple PWM driver. -pub struct RingBufferedPwm<'d, T: GeneralInstance4Channel> { - inner: Timer<'d, T>, -} - -impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { - pub fn into_ring_buffered_channel(mut self, tx_dma: Peri<'_, impl super::Dma>, dma_buf: &'d mut [u8]) -> RingBufferedPwmChannel<'d> { - assert!(!dma_buf.is_empty() && dma_buf.len() <= 0xFFFF); - let ring_buf = WritableDmaRingBuffer::new(dma_buf); - let channel = C::CHANNEL; - RingBufferedPwmChannel { - timer: unsafe { self.inner.clone_unchecked() }, - channel, - ring_buf - } - // let ring_buf = WriteableRingBuffer::new(); - } -} diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index 56d00ea59..53e0345ea 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -4,8 +4,13 @@ use core::marker::PhantomData; use core::mem::ManuallyDrop; use super::low_level::{CountingMode, OutputCompareMode, OutputPolarity, Timer}; +use super::ringbuffered::RingBufferedPwmChannel; use super::{Ch1, Ch2, Ch3, Ch4, Channel, GeneralInstance4Channel, TimerChannel, TimerPin}; +use crate::dma::WritableRingBuffer; use crate::Peri; +#[cfg(not(any(bdma, gpdma)))] +use crate::dma::{Burst, FifoThreshold}; +use crate::dma::TransferOptions; #[cfg(gpio_v2)] use crate::gpio::Pull; use crate::gpio::{AfType, AnyPin, OutputType, Speed}; @@ -374,6 +379,36 @@ impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { self.inner.waveform_continuous(dma, duty).await; } + pub fn into_ring_buffered_channel(self, tx_dma: Peri<'d, impl super::Dma>, dma_buf: &'d mut [u16]) -> RingBufferedPwmChannel<'d, T> { + assert!(!dma_buf.is_empty() && dma_buf.len() <= 0xFFFF); + + let channel = C::CHANNEL; + let request = tx_dma.request(); + + let opts = TransferOptions { + #[cfg(not(any(bdma, gpdma)))] + fifo_threshold: Some(FifoThreshold::Full), + #[cfg(not(any(bdma, gpdma)))] + mburst: Burst::Incr8, + ..Default::default() + }; + + let ring_buf = unsafe { + WritableRingBuffer::new( + tx_dma, + request, + self.inner.regs_gp16().ccr(channel.index()).as_ptr() as *mut u16, + dma_buf, + opts, + ) + }; + + RingBufferedPwmChannel::new( + unsafe { self.inner.clone_unchecked() }, + channel, + ring_buf + ) + } } impl<'d, T: GeneralInstance4Channel> embedded_hal_1::pwm::ErrorType for SimplePwmChannel<'d, T> { -- cgit From 05417e91093dfd7e150083738988259d66ee4e37 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Mon, 24 Nov 2025 21:54:25 +0100 Subject: fix: Warnings and formatting all fixed --- embassy-stm32/src/timer/low_level.rs | 4 +--- embassy-stm32/src/timer/ringbuffered.rs | 25 +++++++++++++++++++++---- embassy-stm32/src/timer/simple_pwm.rs | 31 +++++++++++++++++++++++-------- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/embassy-stm32/src/timer/low_level.rs b/embassy-stm32/src/timer/low_level.rs index 2cee5f1f5..d1cf11386 100644 --- a/embassy-stm32/src/timer/low_level.rs +++ b/embassy-stm32/src/timer/low_level.rs @@ -812,8 +812,6 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { /// You may want to start this in a new thread as this will block forever pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { - - use crate::pac::timer::vals::Ccds; #[allow(clippy::let_unit_value)] // eg. stm32f334 @@ -855,7 +853,7 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { req: dma::Request, channel: Channel, duty: &[u16], - circular: bool, + #[allow(unused_variables)] circular: bool, ) { let original_duty_state = self.get_compare_value(channel); let original_enable_state = self.get_channel_enable_state(channel); diff --git a/embassy-stm32/src/timer/ringbuffered.rs b/embassy-stm32/src/timer/ringbuffered.rs index bb602f8a7..e8f97bf59 100644 --- a/embassy-stm32/src/timer/ringbuffered.rs +++ b/embassy-stm32/src/timer/ringbuffered.rs @@ -5,9 +5,24 @@ use core::task::Waker; use super::low_level::Timer; use super::{Channel, GeneralInstance4Channel}; -use crate::dma::ringbuffer::Error; use crate::dma::WritableRingBuffer; +use crate::dma::ringbuffer::Error; +/// A PWM channel that uses a DMA ring buffer for continuous waveform generation. +/// +/// This allows you to continuously update PWM duty cycles via DMA without blocking the CPU. +/// The ring buffer enables smooth, uninterrupted waveform generation by automatically cycling +/// through duty cycle values stored in memory. +/// +/// You can write new duty cycle values to the ring buffer while it's running, enabling +/// dynamic waveform generation for applications like motor control, LED dimming, or audio output. +/// +/// # Example +/// ```ignore +/// let mut channel = pwm.ch1().into_ring_buffered_channel(dma_ch, &mut buffer); +/// channel.start(); // Start DMA transfer +/// channel.write(&[100, 200, 300]).ok(); // Update duty cycles +/// ``` pub struct RingBufferedPwmChannel<'d, T: GeneralInstance4Channel> { timer: ManuallyDrop>, ring_buf: WritableRingBuffer<'d, u16>, @@ -15,7 +30,11 @@ pub struct RingBufferedPwmChannel<'d, T: GeneralInstance4Channel> { } impl<'d, T: GeneralInstance4Channel> RingBufferedPwmChannel<'d, T> { - pub(crate) fn new(timer: ManuallyDrop>, channel: Channel, ring_buf: WritableRingBuffer<'d, u16>) -> Self { + pub(crate) fn new( + timer: ManuallyDrop>, + channel: Channel, + ring_buf: WritableRingBuffer<'d, u16>, + ) -> Self { Self { timer, ring_buf, @@ -148,5 +167,3 @@ pub struct RingBufferedPwmChannels<'d, T: GeneralInstance4Channel> { /// Channel 4 pub ch4: RingBufferedPwmChannel<'d, T>, } - - diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index 53e0345ea..f4656b7bd 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -6,11 +6,11 @@ use core::mem::ManuallyDrop; use super::low_level::{CountingMode, OutputCompareMode, OutputPolarity, Timer}; use super::ringbuffered::RingBufferedPwmChannel; use super::{Ch1, Ch2, Ch3, Ch4, Channel, GeneralInstance4Channel, TimerChannel, TimerPin}; -use crate::dma::WritableRingBuffer; use crate::Peri; +use crate::dma::TransferOptions; +use crate::dma::WritableRingBuffer; #[cfg(not(any(bdma, gpdma)))] use crate::dma::{Burst, FifoThreshold}; -use crate::dma::TransferOptions; #[cfg(gpio_v2)] use crate::gpio::Pull; use crate::gpio::{AfType, AnyPin, OutputType, Speed}; @@ -379,9 +379,27 @@ impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { self.inner.waveform_continuous(dma, duty).await; } - pub fn into_ring_buffered_channel(self, tx_dma: Peri<'d, impl super::Dma>, dma_buf: &'d mut [u16]) -> RingBufferedPwmChannel<'d, T> { + + /// Convert this PWM channel into a ring-buffered PWM channel. + /// + /// This allows continuous PWM waveform generation using a DMA ring buffer. + /// The ring buffer enables dynamic updates to the PWM duty cycle without blocking. + /// + /// # Arguments + /// * `tx_dma` - The DMA channel to use for transferring duty cycle values + /// * `dma_buf` - The buffer to use as a ring buffer (must be non-empty and <= 65535 elements) + /// + /// # Panics + /// Panics if `dma_buf` is empty or longer than 65535 elements. + pub fn into_ring_buffered_channel( + self, + tx_dma: Peri<'d, impl super::Dma>, + dma_buf: &'d mut [u16], + ) -> RingBufferedPwmChannel<'d, T> { assert!(!dma_buf.is_empty() && dma_buf.len() <= 0xFFFF); + use crate::pac::timer::vals::Ccds; + let channel = C::CHANNEL; let request = tx_dma.request(); @@ -393,6 +411,7 @@ impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { ..Default::default() }; + self.inner.set_cc_dma_selection(Ccds::ON_UPDATE); let ring_buf = unsafe { WritableRingBuffer::new( tx_dma, @@ -403,11 +422,7 @@ impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { ) }; - RingBufferedPwmChannel::new( - unsafe { self.inner.clone_unchecked() }, - channel, - ring_buf - ) + RingBufferedPwmChannel::new(unsafe { self.inner.clone_unchecked() }, channel, ring_buf) } } -- cgit From 6a97b6718e1389ce9e5dd3fb989b3b9f3fcfbd09 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Mon, 24 Nov 2025 22:31:04 +0100 Subject: wip: Add a working example for the stm32f767zi nucleo. Currently no PWM signal visible... --- embassy-stm32/src/timer/simple_pwm.rs | 2 + examples/stm32f7/src/bin/pwm.rs | 31 +++--- examples/stm32f7/src/bin/pwm_ringbuffer.rs | 157 +++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 examples/stm32f7/src/bin/pwm_ringbuffer.rs diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index f4656b7bd..8b738741a 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -412,6 +412,8 @@ impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { }; self.inner.set_cc_dma_selection(Ccds::ON_UPDATE); + self.inner.set_cc_dma_enable_state(channel, true); + let ring_buf = unsafe { WritableRingBuffer::new( tx_dma, diff --git a/examples/stm32f7/src/bin/pwm.rs b/examples/stm32f7/src/bin/pwm.rs index 53efc2d8a..c1c7ec6ce 100644 --- a/examples/stm32f7/src/bin/pwm.rs +++ b/examples/stm32f7/src/bin/pwm.rs @@ -8,6 +8,7 @@ use embassy_stm32::gpio::OutputType; use embassy_stm32::time::Hertz; use embassy_stm32::time::mhz; use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; +use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; // If you are trying this and your USB device doesn't connect, the most @@ -41,29 +42,21 @@ async fn main(_spawner: Spawner) { } let p = embassy_stm32::init(config); let ch1_pin = PwmPin::new(p.PE9, OutputType::PushPull); - let ch2_pin = PwmPin::new(p.PE11, OutputType::PushPull); - let mut pwm = SimplePwm::new( - p.TIM1, - Some(ch1_pin), - Some(ch2_pin), - None, - None, - mhz(1), - Default::default(), - ); + let mut pwm = SimplePwm::new(p.TIM1, Some(ch1_pin), None, None, None, mhz(1), Default::default()); let mut ch1 = pwm.ch1(); ch1.enable(); + info!("PWM initialized"); info!("PWM max duty {}", ch1.max_duty_cycle()); - info!("PWM duty on channel 1 (D6) 50%"); - ch1.set_duty_cycle_fraction(1, 2); - info!("PWM waveform on channel 2 (D5)"); - const MAX_DUTY: usize = 200; - let mut duty = [0u16; MAX_DUTY]; - for i in 0..MAX_DUTY { - duty[i] = i as u16; + loop { + ch1.set_duty_cycle_fully_off(); + Timer::after_millis(300).await; + ch1.set_duty_cycle_fraction(1, 4); + Timer::after_millis(300).await; + ch1.set_duty_cycle_fraction(1, 2); + Timer::after_millis(300).await; + ch1.set_duty_cycle(ch1.max_duty_cycle() - 1); + Timer::after_millis(300).await; } - pwm.waveform_continuous::(p.DMA2_CH6, &duty) - .await; } diff --git a/examples/stm32f7/src/bin/pwm_ringbuffer.rs b/examples/stm32f7/src/bin/pwm_ringbuffer.rs new file mode 100644 index 000000000..a8744f190 --- /dev/null +++ b/examples/stm32f7/src/bin/pwm_ringbuffer.rs @@ -0,0 +1,157 @@ +#![no_std] +#![no_main] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_stm32::Config; +use embassy_stm32::gpio::OutputType; +use embassy_stm32::time::mhz; +use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +// If you are trying this and your USB device doesn't connect, the most +// common issues are the RCC config and vbus_detection +// +// See https://embassy.dev/book/#_the_usb_examples_are_not_working_on_my_board_is_there_anything_else_i_need_to_configure +// for more information. +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + info!("PWM Ring Buffer Example"); + + let mut config = Config::default(); + { + use embassy_stm32::rcc::*; + use embassy_stm32::time::Hertz; + config.rcc.hse = Some(Hse { + freq: Hertz(8_000_000), + mode: HseMode::Bypass, + }); + config.rcc.pll_src = PllSource::HSE; + config.rcc.pll = Some(Pll { + prediv: PllPreDiv::DIV4, + mul: PllMul::MUL200, + divp: Some(PllPDiv::DIV2), // 8mhz / 4 * 200 / 2 = 200Mhz + divq: Some(PllQDiv::DIV4), // 8mhz / 4 * 200 / 4 = 100Mhz + divr: None, + }); + config.rcc.ahb_pre = AHBPrescaler::DIV1; + config.rcc.apb1_pre = APBPrescaler::DIV4; + config.rcc.apb2_pre = APBPrescaler::DIV2; + config.rcc.sys = Sysclk::PLL1_P; + } + let p = embassy_stm32::init(config); + + // Initialize PWM on TIM1 + let ch1_pin = PwmPin::new(p.PE9, OutputType::PushPull); + let ch2_pin = PwmPin::new(p.PE11, OutputType::PushPull); + let mut pwm = SimplePwm::new( + p.TIM1, + Some(ch1_pin), + Some(ch2_pin), + None, + None, + mhz(1), + Default::default(), + ); + + // Use channel 1 for static PWM at 50% + let mut ch1 = pwm.ch1(); + ch1.enable(); + ch1.set_duty_cycle_fraction(1, 2); + info!("Channel 1 (PE9/D6): Static 50% duty cycle"); + + // Get max duty from channel 1 before converting channel 2 + let max_duty = ch1.max_duty_cycle(); + info!("PWM max duty: {}", max_duty); + + // Create a DMA ring buffer for channel 2 + const BUFFER_SIZE: usize = 128; + static mut DMA_BUFFER: [u16; BUFFER_SIZE] = [0u16; BUFFER_SIZE]; + let dma_buffer = unsafe { &mut *core::ptr::addr_of_mut!(DMA_BUFFER) }; + + // Pre-fill buffer with initial sine wave using lookup table approach + for i in 0..BUFFER_SIZE { + // Simple sine approximation using triangle wave + let phase = (i * 256) / BUFFER_SIZE; + let sine_approx = if phase < 128 { + phase as u16 * 2 + } else { + (255 - phase) as u16 * 2 + }; + dma_buffer[i] = (sine_approx as u32 * max_duty as u32 / 256) as u16; + } + + // Convert channel 2 to ring-buffered PWM + let mut ring_pwm = pwm.into_ring_buffered_channel::(p.DMA2_CH6, dma_buffer); + + info!("Ring buffer capacity: {}", ring_pwm.capacity()); + + // Enable the PWM channel output + ring_pwm.enable(); + + // Pre-write some initial data to the buffer before starting + info!("Pre-writing initial waveform data..."); + + // Start the DMA ring buffer + ring_pwm.start(); + info!("Channel 2 (PE11/D5): Ring buffered sine wave started"); + + // Give DMA time to start consuming + Timer::after_millis(10).await; + + // Continuously update the waveform + let mut phase: f32 = 0.0; + let mut amplitude: f32 = 1.0; + let mut amplitude_direction = -0.05; + + loop { + Timer::after_millis(50).await; + + // Generate new waveform data with varying amplitude + let mut new_data = [0u16; 32]; + for i in 0..new_data.len() { + // Triangle wave approximation for sine + let pos = ((i as u32 + phase as u32) * 4) % 256; + let sine_approx = if pos < 128 { + pos as u16 * 2 + } else { + (255 - pos) as u16 * 2 + }; + let scaled = (sine_approx as u32 * (amplitude * 256.0) as u32) / (256 * 256); + new_data[i] = ((scaled * max_duty as u32) / 256) as u16; + } + + // Write new data to the ring buffer + match ring_pwm.write(&new_data) { + Ok((written, _remaining)) => { + if written < new_data.len() { + info!("Ring buffer getting full, wrote {} of {}", written, new_data.len()); + } + } + Err(e) => { + info!("Write error: {:?}", e); + } + } + + // Update phase for animation effect + phase += 2.0; + if phase >= 64.0 { + phase = 0.0; + } + + // Vary amplitude for breathing effect + amplitude += amplitude_direction; + if amplitude <= 0.2 || amplitude >= 1.0 { + amplitude_direction = -amplitude_direction; + } + + // Log buffer status periodically + if (phase as u32) % 10 == 0 { + match ring_pwm.len() { + Ok(len) => info!("Ring buffer fill: {}/{}", len, ring_pwm.capacity()), + Err(_) => info!("Error reading buffer length"), + } + } + } +} -- cgit From 96ffbec30a0d17470dd93a6c50f4264060bf1e69 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Mon, 24 Nov 2025 22:44:56 +0100 Subject: fix: formatting --- embassy-stm32/src/timer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embassy-stm32/src/timer/mod.rs b/embassy-stm32/src/timer/mod.rs index aef3598f1..3fa363881 100644 --- a/embassy-stm32/src/timer/mod.rs +++ b/embassy-stm32/src/timer/mod.rs @@ -12,8 +12,8 @@ pub mod low_level; pub mod one_pulse; pub mod pwm_input; pub mod qei; -pub mod simple_pwm; pub mod ringbuffered; +pub mod simple_pwm; use crate::interrupt; use crate::rcc::RccPeripheral; -- cgit From ff939095a2a883361346ea0cf6f2b6f9d1d24936 Mon Sep 17 00:00:00 2001 From: Eicke Hecht Date: Mon, 24 Nov 2025 23:03:02 +0100 Subject: fix: Formatting, use nightly... --- embassy-stm32/src/timer/simple_pwm.rs | 3 +-- examples/stm32f7/src/bin/pwm.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index 8b738741a..bbe7ef595 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -7,10 +7,9 @@ use super::low_level::{CountingMode, OutputCompareMode, OutputPolarity, Timer}; use super::ringbuffered::RingBufferedPwmChannel; use super::{Ch1, Ch2, Ch3, Ch4, Channel, GeneralInstance4Channel, TimerChannel, TimerPin}; use crate::Peri; -use crate::dma::TransferOptions; -use crate::dma::WritableRingBuffer; #[cfg(not(any(bdma, gpdma)))] use crate::dma::{Burst, FifoThreshold}; +use crate::dma::{TransferOptions, WritableRingBuffer}; #[cfg(gpio_v2)] use crate::gpio::Pull; use crate::gpio::{AfType, AnyPin, OutputType, Speed}; diff --git a/examples/stm32f7/src/bin/pwm.rs b/examples/stm32f7/src/bin/pwm.rs index c1c7ec6ce..b071eb597 100644 --- a/examples/stm32f7/src/bin/pwm.rs +++ b/examples/stm32f7/src/bin/pwm.rs @@ -5,8 +5,7 @@ use defmt::*; use embassy_executor::Spawner; use embassy_stm32::Config; use embassy_stm32::gpio::OutputType; -use embassy_stm32::time::Hertz; -use embassy_stm32::time::mhz; +use embassy_stm32::time::{Hertz, mhz}; use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; use embassy_time::Timer; use {defmt_rtt as _, panic_probe as _}; -- cgit From 2e70c376c884a64fc931406350fecb6c0314dcf0 Mon Sep 17 00:00:00 2001 From: xoviat Date: Tue, 25 Nov 2025 10:14:55 -0600 Subject: timer: add writable ring buffer --- embassy-stm32/CHANGELOG.md | 1 + embassy-stm32/src/timer/low_level.rs | 35 +++++- embassy-stm32/src/timer/mod.rs | 1 + embassy-stm32/src/timer/ringbuffered.rs | 169 +++++++++++++++++++++++++++++ embassy-stm32/src/timer/simple_pwm.rs | 31 ++++++ examples/stm32f7/src/bin/pwm.rs | 61 +++++++++++ examples/stm32f7/src/bin/pwm_ringbuffer.rs | 153 ++++++++++++++++++++++++++ 7 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 embassy-stm32/src/timer/ringbuffered.rs create mode 100644 examples/stm32f7/src/bin/pwm.rs create mode 100644 examples/stm32f7/src/bin/pwm_ringbuffer.rs diff --git a/embassy-stm32/CHANGELOG.md b/embassy-stm32/CHANGELOG.md index 949ea03b5..d3e5ba48d 100644 --- a/embassy-stm32/CHANGELOG.md +++ b/embassy-stm32/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - ReleaseDate +- feat: Add continuous waveform method to SimplePWM - change: remove waveform timer method - change: low power: store stop mode for dma channels - fix: Fixed ADC4 enable() for WBA diff --git a/embassy-stm32/src/timer/low_level.rs b/embassy-stm32/src/timer/low_level.rs index f986c8dab..aba08081f 100644 --- a/embassy-stm32/src/timer/low_level.rs +++ b/embassy-stm32/src/timer/low_level.rs @@ -13,7 +13,7 @@ use embassy_hal_internal::Peri; pub use stm32_metapac::timer::vals::{FilterValue, Mms as MasterMode, Sms as SlaveMode, Ts as TriggerSource}; use super::*; -use crate::dma::Transfer; +use crate::dma::{Transfer, WritableRingBuffer}; use crate::pac::timer::vals; use crate::rcc; use crate::time::Hertz; @@ -660,6 +660,39 @@ impl<'d, T: GeneralInstance4Channel> Timer<'d, T> { } } + /// Setup a ring buffer for the channel + pub fn setup_ring_buffer<'a>( + &mut self, + dma: Peri<'a, impl super::UpDma>, + channel: Channel, + dma_buf: &'a mut [u16], + ) -> WritableRingBuffer<'a, u16> { + #[allow(clippy::let_unit_value)] // eg. stm32f334 + let req = dma.request(); + + unsafe { + use crate::dma::TransferOptions; + #[cfg(not(any(bdma, gpdma)))] + use crate::dma::{Burst, FifoThreshold}; + + let dma_transfer_option = TransferOptions { + #[cfg(not(any(bdma, gpdma)))] + fifo_threshold: Some(FifoThreshold::Full), + #[cfg(not(any(bdma, gpdma)))] + mburst: Burst::Incr8, + ..Default::default() + }; + + WritableRingBuffer::new( + dma, + req, + self.regs_1ch().ccr(channel.index()).as_ptr() as *mut u16, + dma_buf, + dma_transfer_option, + ) + } + } + /// Generate a sequence of PWM waveform /// /// Note: diff --git a/embassy-stm32/src/timer/mod.rs b/embassy-stm32/src/timer/mod.rs index 804d1ef37..3fa363881 100644 --- a/embassy-stm32/src/timer/mod.rs +++ b/embassy-stm32/src/timer/mod.rs @@ -12,6 +12,7 @@ pub mod low_level; pub mod one_pulse; pub mod pwm_input; pub mod qei; +pub mod ringbuffered; pub mod simple_pwm; use crate::interrupt; diff --git a/embassy-stm32/src/timer/ringbuffered.rs b/embassy-stm32/src/timer/ringbuffered.rs new file mode 100644 index 000000000..e8f97bf59 --- /dev/null +++ b/embassy-stm32/src/timer/ringbuffered.rs @@ -0,0 +1,169 @@ +//! RingBuffered PWM driver. + +use core::mem::ManuallyDrop; +use core::task::Waker; + +use super::low_level::Timer; +use super::{Channel, GeneralInstance4Channel}; +use crate::dma::WritableRingBuffer; +use crate::dma::ringbuffer::Error; + +/// A PWM channel that uses a DMA ring buffer for continuous waveform generation. +/// +/// This allows you to continuously update PWM duty cycles via DMA without blocking the CPU. +/// The ring buffer enables smooth, uninterrupted waveform generation by automatically cycling +/// through duty cycle values stored in memory. +/// +/// You can write new duty cycle values to the ring buffer while it's running, enabling +/// dynamic waveform generation for applications like motor control, LED dimming, or audio output. +/// +/// # Example +/// ```ignore +/// let mut channel = pwm.ch1().into_ring_buffered_channel(dma_ch, &mut buffer); +/// channel.start(); // Start DMA transfer +/// channel.write(&[100, 200, 300]).ok(); // Update duty cycles +/// ``` +pub struct RingBufferedPwmChannel<'d, T: GeneralInstance4Channel> { + timer: ManuallyDrop>, + ring_buf: WritableRingBuffer<'d, u16>, + channel: Channel, +} + +impl<'d, T: GeneralInstance4Channel> RingBufferedPwmChannel<'d, T> { + pub(crate) fn new( + timer: ManuallyDrop>, + channel: Channel, + ring_buf: WritableRingBuffer<'d, u16>, + ) -> Self { + Self { + timer, + ring_buf, + channel, + } + } + + /// Start the ring buffer operation. + /// + /// You must call this after creating it for it to work. + pub fn start(&mut self) { + self.ring_buf.start() + } + + /// Clear all data in the ring buffer. + pub fn clear(&mut self) { + self.ring_buf.clear() + } + + /// Write elements directly to the raw buffer. This can be used to fill the buffer before starting the DMA transfer. + pub fn write_immediate(&mut self, buf: &[u16]) -> Result<(usize, usize), Error> { + self.ring_buf.write_immediate(buf) + } + + /// Write elements from the ring buffer + /// Return a tuple of the length written and the length remaining in the buffer + pub fn write(&mut self, buf: &[u16]) -> Result<(usize, usize), Error> { + self.ring_buf.write(buf) + } + + /// Write an exact number of elements to the ringbuffer. + pub async fn write_exact(&mut self, buffer: &[u16]) -> Result { + self.ring_buf.write_exact(buffer).await + } + + /// Wait for any ring buffer write error. + pub async fn wait_write_error(&mut self) -> Result { + self.ring_buf.wait_write_error().await + } + + /// The current length of the ringbuffer + pub fn len(&mut self) -> Result { + self.ring_buf.len() + } + + /// The capacity of the ringbuffer + pub const fn capacity(&self) -> usize { + self.ring_buf.capacity() + } + + /// Set a waker to be woken when at least one byte is send. + pub fn set_waker(&mut self, waker: &Waker) { + self.ring_buf.set_waker(waker) + } + + /// Request the DMA to reset. The configuration for this channel will not be preserved. + /// + /// This doesn't immediately stop the transfer, you have to wait until is_running returns false. + pub fn request_reset(&mut self) { + self.ring_buf.request_reset() + } + + /// Request the transfer to pause, keeping the existing configuration for this channel. + /// To restart the transfer, call [`start`](Self::start) again. + /// + /// This doesn't immediately stop the transfer, you have to wait until is_running returns false. + pub fn request_pause(&mut self) { + self.ring_buf.request_pause() + } + + /// Return whether DMA is still running. + /// + /// If this returns false, it can be because either the transfer finished, or it was requested to stop early with request_stop. + pub fn is_running(&mut self) -> bool { + self.ring_buf.is_running() + } + + /// Stop the DMA transfer and await until the buffer is empty. + /// + /// This disables the DMA transfer's circular mode so that the transfer stops when all available data has been written. + /// + /// This is designed to be used with streaming output data such as the I2S/SAI or DAC. + pub async fn stop(&mut self) { + self.ring_buf.stop().await + } + + /// Enable the given channel. + pub fn enable(&mut self) { + self.timer.enable_channel(self.channel, true); + } + + /// Disable the given channel. + pub fn disable(&mut self) { + self.timer.enable_channel(self.channel, false); + } + + /// Check whether given channel is enabled + pub fn is_enabled(&self) -> bool { + self.timer.get_channel_enable_state(self.channel) + } + + /// Get max duty value. + /// + /// This value depends on the configured frequency and the timer's clock rate from RCC. + pub fn max_duty_cycle(&self) -> u16 { + let max = self.timer.get_max_compare_value(); + assert!(max < u16::MAX as u32); + max as u16 + 1 + } + + /// Set the output polarity for a given channel. + pub fn set_polarity(&mut self, polarity: super::low_level::OutputPolarity) { + self.timer.set_output_polarity(self.channel, polarity); + } + + /// Set the output compare mode for a given channel. + pub fn set_output_compare_mode(&mut self, mode: super::low_level::OutputCompareMode) { + self.timer.set_output_compare_mode(self.channel, mode); + } +} + +/// A group of four [`SimplePwmChannel`]s, obtained from [`SimplePwm::split`]. +pub struct RingBufferedPwmChannels<'d, T: GeneralInstance4Channel> { + /// Channel 1 + pub ch1: RingBufferedPwmChannel<'d, T>, + /// Channel 2 + pub ch2: RingBufferedPwmChannel<'d, T>, + /// Channel 3 + pub ch3: RingBufferedPwmChannel<'d, T>, + /// Channel 4 + pub ch4: RingBufferedPwmChannel<'d, T>, +} diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index eb1b66358..bc33a52ea 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -4,8 +4,12 @@ use core::marker::PhantomData; use core::mem::ManuallyDrop; use super::low_level::{CountingMode, OutputCompareMode, OutputPolarity, Timer}; +use super::ringbuffered::RingBufferedPwmChannel; use super::{Ch1, Ch2, Ch3, Ch4, Channel, GeneralInstance4Channel, TimerChannel, TimerPin}; use crate::Peri; +#[cfg(not(any(bdma, gpdma)))] +use crate::dma::{Burst, FifoThreshold}; +use crate::dma::{TransferOptions, WritableRingBuffer}; #[cfg(gpio_v2)] use crate::gpio::Pull; use crate::gpio::{AfType, AnyPin, OutputType, Speed}; @@ -158,6 +162,33 @@ impl<'d, T: GeneralInstance4Channel> SimplePwmChannel<'d, T> { pub fn set_output_compare_mode(&mut self, mode: OutputCompareMode) { self.timer.set_output_compare_mode(self.channel, mode); } + + /// Convert this PWM channel into a ring-buffered PWM channel. + /// + /// This allows continuous PWM waveform generation using a DMA ring buffer. + /// The ring buffer enables dynamic updates to the PWM duty cycle without blocking. + /// + /// # Arguments + /// * `tx_dma` - The DMA channel to use for transferring duty cycle values + /// * `dma_buf` - The buffer to use as a ring buffer (must be non-empty and <= 65535 elements) + /// + /// # Panics + /// Panics if `dma_buf` is empty or longer than 65535 elements. + pub fn into_ring_buffered_channel( + mut self, + tx_dma: Peri<'d, impl super::UpDma>, + dma_buf: &'d mut [u16], + ) -> RingBufferedPwmChannel<'d, T> { + assert!(!dma_buf.is_empty() && dma_buf.len() <= 0xFFFF); + + self.timer.enable_update_dma(true); + + RingBufferedPwmChannel::new( + unsafe { self.timer.clone_unchecked() }, + self.channel, + self.timer.setup_ring_buffer(tx_dma, self.channel, dma_buf), + ) + } } /// A group of four [`SimplePwmChannel`]s, obtained from [`SimplePwm::split`]. diff --git a/examples/stm32f7/src/bin/pwm.rs b/examples/stm32f7/src/bin/pwm.rs new file mode 100644 index 000000000..b071eb597 --- /dev/null +++ b/examples/stm32f7/src/bin/pwm.rs @@ -0,0 +1,61 @@ +#![no_std] +#![no_main] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_stm32::Config; +use embassy_stm32::gpio::OutputType; +use embassy_stm32::time::{Hertz, mhz}; +use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +// If you are trying this and your USB device doesn't connect, the most +// common issues are the RCC config and vbus_detection +// +// See https://embassy.dev/book/#_the_usb_examples_are_not_working_on_my_board_is_there_anything_else_i_need_to_configure +// for more information. +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + info!("Hello World!"); + + let mut config = Config::default(); + { + use embassy_stm32::rcc::*; + config.rcc.hse = Some(Hse { + freq: Hertz(8_000_000), + mode: HseMode::Bypass, + }); + config.rcc.pll_src = PllSource::HSE; + config.rcc.pll = Some(Pll { + prediv: PllPreDiv::DIV4, + mul: PllMul::MUL200, + divp: Some(PllPDiv::DIV2), // 8mhz / 4 * 200 / 2 = 200Mhz + divq: Some(PllQDiv::DIV4), // 8mhz / 4 * 200 / 4 = 100Mhz + divr: None, + }); + config.rcc.ahb_pre = AHBPrescaler::DIV1; + config.rcc.apb1_pre = APBPrescaler::DIV4; + config.rcc.apb2_pre = APBPrescaler::DIV2; + config.rcc.sys = Sysclk::PLL1_P; + } + let p = embassy_stm32::init(config); + let ch1_pin = PwmPin::new(p.PE9, OutputType::PushPull); + let mut pwm = SimplePwm::new(p.TIM1, Some(ch1_pin), None, None, None, mhz(1), Default::default()); + let mut ch1 = pwm.ch1(); + ch1.enable(); + + info!("PWM initialized"); + info!("PWM max duty {}", ch1.max_duty_cycle()); + + loop { + ch1.set_duty_cycle_fully_off(); + Timer::after_millis(300).await; + ch1.set_duty_cycle_fraction(1, 4); + Timer::after_millis(300).await; + ch1.set_duty_cycle_fraction(1, 2); + Timer::after_millis(300).await; + ch1.set_duty_cycle(ch1.max_duty_cycle() - 1); + Timer::after_millis(300).await; + } +} diff --git a/examples/stm32f7/src/bin/pwm_ringbuffer.rs b/examples/stm32f7/src/bin/pwm_ringbuffer.rs new file mode 100644 index 000000000..4d191ac13 --- /dev/null +++ b/examples/stm32f7/src/bin/pwm_ringbuffer.rs @@ -0,0 +1,153 @@ +#![no_std] +#![no_main] + +use defmt::*; +use embassy_executor::Spawner; +use embassy_stm32::Config; +use embassy_stm32::gpio::OutputType; +use embassy_stm32::time::mhz; +use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm}; +use embassy_time::Timer; +use {defmt_rtt as _, panic_probe as _}; + +// If you are trying this and your USB device doesn't connect, the most +// common issues are the RCC config and vbus_detection +// +// See https://embassy.dev/book/#_the_usb_examples_are_not_working_on_my_board_is_there_anything_else_i_need_to_configure +// for more information. +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + info!("PWM Ring Buffer Example"); + + let mut config = Config::default(); + { + use embassy_stm32::rcc::*; + use embassy_stm32::time::Hertz; + config.rcc.hse = Some(Hse { + freq: Hertz(8_000_000), + mode: HseMode::Bypass, + }); + config.rcc.pll_src = PllSource::HSE; + config.rcc.pll = Some(Pll { + prediv: PllPreDiv::DIV4, + mul: PllMul::MUL200, + divp: Some(PllPDiv::DIV2), // 8mhz / 4 * 200 / 2 = 200Mhz + divq: Some(PllQDiv::DIV4), // 8mhz / 4 * 200 / 4 = 100Mhz + divr: None, + }); + config.rcc.ahb_pre = AHBPrescaler::DIV1; + config.rcc.apb1_pre = APBPrescaler::DIV4; + config.rcc.apb2_pre = APBPrescaler::DIV2; + config.rcc.sys = Sysclk::PLL1_P; + } + let p = embassy_stm32::init(config); + + // Initialize PWM on TIM1 + let ch1_pin = PwmPin::new(p.PE9, OutputType::PushPull); + let ch2_pin = PwmPin::new(p.PE11, OutputType::PushPull); + let mut pwm = SimplePwm::new( + p.TIM1, + Some(ch1_pin), + Some(ch2_pin), + None, + None, + mhz(1), + Default::default(), + ); + + // Use channel 1 for static PWM at 50% + let mut ch1 = pwm.ch1(); + ch1.enable(); + ch1.set_duty_cycle_fraction(1, 2); + info!("Channel 1 (PE9/D6): Static 50% duty cycle"); + + // Get max duty from channel 1 before converting channel 2 + let max_duty = ch1.max_duty_cycle(); + info!("PWM max duty: {}", max_duty); + + // Create a DMA ring buffer for channel 2 + const BUFFER_SIZE: usize = 128; + static mut DMA_BUFFER: [u16; BUFFER_SIZE] = [0u16; BUFFER_SIZE]; + let dma_buffer = unsafe { &mut *core::ptr::addr_of_mut!(DMA_BUFFER) }; + + // Pre-fill buffer with initial sine wave using lookup table approach + for i in 0..BUFFER_SIZE { + // Simple sine approximation using triangle wave + let phase = (i * 256) / BUFFER_SIZE; + let sine_approx = if phase < 128 { + phase as u16 * 2 + } else { + (255 - phase) as u16 * 2 + }; + dma_buffer[i] = (sine_approx as u32 * max_duty as u32 / 256) as u16; + } + + // Convert channel 2 to ring-buffered PWM + let mut ring_pwm = pwm.ch1().into_ring_buffered_channel(p.DMA2_CH5, dma_buffer); + + info!("Ring buffer capacity: {}", ring_pwm.capacity()); + + // Pre-write some initial data to the buffer before starting + info!("Pre-writing initial waveform data..."); + + ring_pwm.write(&[0; BUFFER_SIZE]).unwrap(); + + // Enable the PWM channel output + ring_pwm.enable(); + + // Start the DMA ring buffer + ring_pwm.start(); + info!("Channel 2 (PE11/D5): Ring buffered sine wave started"); + + // Give DMA time to start consuming + Timer::after_millis(10).await; + + // Continuously update the waveform + let mut phase: f32 = 0.0; + let mut amplitude: f32 = 1.0; + let mut amplitude_direction = -0.05; + + loop { + // Generate new waveform data with varying amplitude + let mut new_data = [0u16; 32]; + for i in 0..new_data.len() { + // Triangle wave approximation for sine + let pos = ((i as u32 + phase as u32) * 4) % 256; + let sine_approx = if pos < 128 { + pos as u16 * 2 + } else { + (255 - pos) as u16 * 2 + }; + let scaled = (sine_approx as u32 * (amplitude * 256.0) as u32) / (256 * 256); + new_data[i] = ((scaled * max_duty as u32) / 256) as u16; + } + + // Write new data to the ring buffer + match ring_pwm.write_exact(&new_data).await { + Ok(_remaining) => {} + Err(e) => { + info!("Write error: {:?}", e); + } + } + + // Update phase for animation effect + phase += 2.0; + if phase >= 64.0 { + phase = 0.0; + } + + // Vary amplitude for breathing effect + amplitude += amplitude_direction; + if amplitude <= 0.2 || amplitude >= 1.0 { + amplitude_direction = -amplitude_direction; + } + + // Log buffer status periodically + if (phase as u32) % 10 == 0 { + match ring_pwm.len() { + Ok(len) => info!("Ring buffer fill: {}/{}", len, ring_pwm.capacity()), + Err(_) => info!("Error reading buffer length"), + } + } + } +} -- cgit From b2ec93caf32153157fd87cceccaaec460ada7aaa Mon Sep 17 00:00:00 2001 From: xoviat Date: Tue, 25 Nov 2025 10:18:55 -0600 Subject: pwm: cleanup --- embassy-stm32/src/timer/simple_pwm.rs | 53 ----------------------------------- 1 file changed, 53 deletions(-) diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index bd0b1d7de..bc33a52ea 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -396,59 +396,6 @@ impl<'d, T: GeneralInstance4Channel> SimplePwm<'d, T> { .await; self.inner.enable_update_dma(false); } - /// Generate a sequence of PWM waveform that will run continously - /// You may want to start this in a new thread as this will block forever - #[inline(always)] - pub async fn waveform_continuous(&mut self, dma: Peri<'_, impl super::Dma>, duty: &[u16]) { - self.inner.waveform_continuous(dma, duty).await; - } - - /// Convert this PWM channel into a ring-buffered PWM channel. - /// - /// This allows continuous PWM waveform generation using a DMA ring buffer. - /// The ring buffer enables dynamic updates to the PWM duty cycle without blocking. - /// - /// # Arguments - /// * `tx_dma` - The DMA channel to use for transferring duty cycle values - /// * `dma_buf` - The buffer to use as a ring buffer (must be non-empty and <= 65535 elements) - /// - /// # Panics - /// Panics if `dma_buf` is empty or longer than 65535 elements. - pub fn into_ring_buffered_channel( - self, - tx_dma: Peri<'d, impl super::Dma>, - dma_buf: &'d mut [u16], - ) -> RingBufferedPwmChannel<'d, T> { - assert!(!dma_buf.is_empty() && dma_buf.len() <= 0xFFFF); - - use crate::pac::timer::vals::Ccds; - - let channel = C::CHANNEL; - let request = tx_dma.request(); - - let opts = TransferOptions { - #[cfg(not(any(bdma, gpdma)))] - fifo_threshold: Some(FifoThreshold::Full), - #[cfg(not(any(bdma, gpdma)))] - mburst: Burst::Incr8, - ..Default::default() - }; - - self.inner.set_cc_dma_selection(Ccds::ON_UPDATE); - self.inner.set_cc_dma_enable_state(channel, true); - - let ring_buf = unsafe { - WritableRingBuffer::new( - tx_dma, - request, - self.inner.regs_gp16().ccr(channel.index()).as_ptr() as *mut u16, - dma_buf, - opts, - ) - }; - - RingBufferedPwmChannel::new(unsafe { self.inner.clone_unchecked() }, channel, ring_buf) - } } impl<'d, T: GeneralInstance4Channel> embedded_hal_1::pwm::ErrorType for SimplePwmChannel<'d, T> { -- cgit From 309ee44c7484c4d11adc3fbd527536027eac8a94 Mon Sep 17 00:00:00 2001 From: xoviat Date: Tue, 25 Nov 2025 10:25:06 -0600 Subject: fmt --- embassy-stm32/src/timer/simple_pwm.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/embassy-stm32/src/timer/simple_pwm.rs b/embassy-stm32/src/timer/simple_pwm.rs index bc33a52ea..484e9fd81 100644 --- a/embassy-stm32/src/timer/simple_pwm.rs +++ b/embassy-stm32/src/timer/simple_pwm.rs @@ -7,9 +7,6 @@ use super::low_level::{CountingMode, OutputCompareMode, OutputPolarity, Timer}; use super::ringbuffered::RingBufferedPwmChannel; use super::{Ch1, Ch2, Ch3, Ch4, Channel, GeneralInstance4Channel, TimerChannel, TimerPin}; use crate::Peri; -#[cfg(not(any(bdma, gpdma)))] -use crate::dma::{Burst, FifoThreshold}; -use crate::dma::{TransferOptions, WritableRingBuffer}; #[cfg(gpio_v2)] use crate::gpio::Pull; use crate::gpio::{AfType, AnyPin, OutputType, Speed}; -- cgit