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