From 5657073293b8995c11cdaf175c82d54d14dc8050 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Wed, 31 Jan 2024 13:39:29 +0000 Subject: [PATCH] RMT symbols and musical buzzer example bugfix for ESP32 (#354) * rmt: wrap rmt_item32_t in a newtype This makes it much easier to use `TxRmtDriver::start_iter` and `TxRmtDriver::start_iter_blocking` (#113). * examples/rmt_musical_buzzer: fix for ESP32 Possibly broken by #97, possibly never worked. The ESP-IDF repo specifically excludes the ESP32 from the RMT musical buzzer demo because its RMT unit lacks hardware looping. Before this change, the demo would simply hang when trying to play the second note possibly because the ESP32 RMT unit does not support asynchronous stopping. As an added bonus, this example is now much easier to modify to experiment with 1-bit chiptune synthesis by making every symbol (cycle) accessible in the ISR iterator. * rmt: remove Pulse::into_rmt_item in favor of Symbol::new Also, factor Pulse pairs into function arguments for ease of use. --- examples/rmt_musical_buzzer.rs | 93 +++++++++++++++++++++++----------- src/rmt.rs | 69 +++++++++++++++---------- 2 files changed, 106 insertions(+), 56 deletions(-) diff --git a/examples/rmt_musical_buzzer.rs b/examples/rmt_musical_buzzer.rs index 6606bffd579..8e70c15911d 100644 --- a/examples/rmt_musical_buzzer.rs +++ b/examples/rmt_musical_buzzer.rs @@ -8,9 +8,9 @@ use core::time::Duration; use esp_idf_hal::delay::Ets; use esp_idf_hal::peripherals::Peripherals; -use esp_idf_hal::rmt::config::{Loop, TransmitConfig}; -use esp_idf_hal::rmt::*; +use esp_idf_hal::rmt::{self, config::TransmitConfig, TxRmtDriver}; +use esp_idf_hal::units::Hertz; use notes::*; fn main() -> anyhow::Result<()> { @@ -19,7 +19,7 @@ fn main() -> anyhow::Result<()> { let peripherals = Peripherals::take()?; let led = peripherals.pins.gpio17; let channel = peripherals.rmt.channel0; - let config = TransmitConfig::new().looping(Loop::Endless); + let config = TransmitConfig::new(); let mut tx: TxRmtDriver<'static> = TxRmtDriver::new(channel, led, &config)?; loop { @@ -30,45 +30,68 @@ fn main() -> anyhow::Result<()> { pub fn play_song(tx: &mut TxRmtDriver<'static>, song: &[NoteValue]) -> anyhow::Result<()> { for note_value in song { - play_note(tx, note_value.note.0, note_value.duration)?; + note_value.play(tx)?; } Ok(()) } -pub fn play_note( - tx: &mut TxRmtDriver<'static>, - pitch: u16, - duration: Duration, -) -> anyhow::Result<()> { - // Calculate the frequency for a piezo buzzer. - let ticks_hz = tx.counter_clock()?; - let tick_count = (ticks_hz.0 as u128 / pitch as u128 / 2_u128) as u16; - let ticks = PulseTicks::new(tick_count)?; - - // Add high and low pulses for the tick duration. - let on = Pulse::new(PinState::High, ticks); - let off = Pulse::new(PinState::Low, ticks); - let mut signal = FixedLengthSignal::<1>::new(); - signal.set(0, &(on, off))?; - - // Play the note for the 80% of the duration. - tx.start(signal)?; - Ets::delay_ms((80 * duration.as_millis() / 100) as u32); - - // Small pause between notes, 20% of the specified duration. - tx.stop()?; - Ets::delay_ms((20 * duration.as_millis() / 100) as u32); +pub struct NoteValueIter { + ticks: rmt::PulseTicks, + tone_cycles: u32, + pause_cycles: u32, +} - Ok(()) +impl NoteValueIter { + fn new(ticks_per_sec: Hertz, note: &NoteValue) -> Self { + // Calculate the frequency for a piezo buzzer. + let dur_ms = note.duration.as_millis(); + let cycles_per_sec = note.note.0; // pitch + let ticks_per_cycle = ticks_per_sec.0 as u128 / cycles_per_sec as u128; + let ticks_per_half = (ticks_per_cycle / 2_u128) as u16; + let ticks = rmt::PulseTicks::new(ticks_per_half).unwrap(); + + let total_cycles = (cycles_per_sec as u128 * dur_ms / 1000_u128) as u32; + // Pause for the last 40ms of every note + let pause_cycles = (cycles_per_sec as u128 * 40_u128 / 1000_u128) as u32; + let tone_cycles = total_cycles - pause_cycles; + + Self { + ticks, + tone_cycles, + pause_cycles, + } + } +} + +impl std::iter::Iterator for NoteValueIter { + type Item = rmt::Symbol; + + // runs in ISR + fn next(&mut self) -> Option { + if self.tone_cycles + self.pause_cycles > 0 { + let high_state = if self.tone_cycles > 0 { + self.tone_cycles -= 1; + rmt::PinState::High + } else { + self.pause_cycles -= 1; + rmt::PinState::Low + }; + let level0 = rmt::Pulse::new(high_state, self.ticks); + let level1 = rmt::Pulse::new(rmt::PinState::Low, self.ticks); + Some(rmt::Symbol::new(level0, level1)) + } else { + None + } + } } #[derive(Debug)] pub struct Note(u16); +#[allow(dead_code)] pub mod notes { use crate::Note; - #[allow(dead_code)] pub const A4: Note = Note(440); pub const AS4: Note = Note(466); pub const B4: Note = Note(494); @@ -90,6 +113,18 @@ pub struct NoteValue { duration: Duration, } +impl NoteValue { + pub fn play(&self, tx: &mut TxRmtDriver<'static>) -> anyhow::Result<()> { + let ticks_hz = tx.counter_clock()?; + tx.start_iter_blocking(self.iter(ticks_hz))?; + Ok(()) + } + + pub fn iter(&self, ticks_hz: Hertz) -> NoteValueIter { + NoteValueIter::new(ticks_hz, self) + } +} + macro_rules! n { ($n: expr, $duration: expr) => { NoteValue { diff --git a/src/rmt.rs b/src/rmt.rs index 1840b74a872..f68bac6a3ff 100644 --- a/src/rmt.rs +++ b/src/rmt.rs @@ -54,8 +54,8 @@ use core::cell::UnsafeCell; use core::convert::{TryFrom, TryInto}; use core::marker::PhantomData; -use core::ptr; use core::time::Duration; +use core::{ptr, slice}; #[cfg(feature = "alloc")] extern crate alloc; @@ -151,21 +151,6 @@ impl Pulse { let ticks = PulseTicks::new_with_duration(ticks_hz, duration)?; Ok(Self::new(pin_state, ticks)) } - - pub fn into_rmt_item(level0: Self, level1: Self) -> rmt_item32_t { - let mut inner_item = rmt_item32_t__bindgen_ty_1__bindgen_ty_1::default(); - - inner_item.set_level0(level0.pin_state as u32); - inner_item.set_duration0(level0.ticks.ticks() as u32); - inner_item.set_level1(level1.pin_state as u32); - inner_item.set_duration1(level1.ticks.ticks() as u32); - - rmt_item32_t { - __bindgen_anon_1: rmt_item32_t__bindgen_ty_1 { - __bindgen_anon_1: inner_item, - }, - } - } } impl Default for Pulse { @@ -641,7 +626,7 @@ impl<'d> TxRmtDriver<'d> { #[cfg(feature = "alloc")] pub fn start_iter(&mut self, iter: T) -> Result<(), EspError> where - T: Iterator + Send + 'static, + T: Iterator + Send + 'static, { let iter = alloc::boxed::Box::new(UnsafeCell::new(iter)); unsafe { @@ -675,7 +660,7 @@ impl<'d> TxRmtDriver<'d> { pub fn start_iter_blocking(&mut self, iter: T) -> Result<(), EspError> where - T: Iterator + Send, + T: Iterator + Send, { let iter = UnsafeCell::new(iter); unsafe { @@ -719,7 +704,7 @@ impl<'d> TxRmtDriver<'d> { translated_size: *mut usize, item_num: *mut usize, ) where - T: Iterator, + T: Iterator, { // An `UnsafeCell` is needed here because we're casting a `*const` to a `*mut`. // Safe because this is the only existing reference. @@ -732,7 +717,7 @@ impl<'d> TxRmtDriver<'d> { } if let Some(item) = iter.next() { - *dest = item; + *dest = item.0; dest = dest.add(1); i += 1; } else { @@ -787,11 +772,47 @@ impl<'d> Drop for TxRmtDriver<'d> { unsafe impl<'d> Send for TxRmtDriver<'d> {} +/// Symbols +/// +/// Represents a single pulse cycle symbol comprised of mark (high) +/// and space (low) periods in either order or a fixed level if both +/// halves have the same [`PinState`]. This is just a newtype over the +/// IDF's `rmt_item32_t` or `rmt_symbol_word_t` type. +pub struct Symbol(rmt_item32_t); + +impl Symbol { + /// Create a symbol from a pair of half-cycles. + pub fn new(level0: Pulse, level1: Pulse) -> Self { + let item = rmt_item32_t { + __bindgen_anon_1: rmt_item32_t__bindgen_ty_1 { val: 0 }, + }; + let mut this = Self(item); + this.update(level0, level1); + this + } + + /// Mutate this symbol to store a different pair of half-cycles. + pub fn update(&mut self, level0: Pulse, level1: Pulse) { + // SAFETY: We're overriding all 32 bits, so it doesn't matter what was here before. + let inner = unsafe { &mut self.0.__bindgen_anon_1.__bindgen_anon_1 }; + inner.set_level0(level0.pin_state as u32); + inner.set_duration0(level0.ticks.0 as u32); + inner.set_level1(level1.pin_state as u32); + inner.set_duration1(level1.ticks.0 as u32); + } +} + /// Signal storage for [`Transmit`] in a format ready for the RMT driver. pub trait Signal { fn as_slice(&self) -> &[rmt_item32_t]; } +impl Signal for Symbol { + fn as_slice(&self) -> &[rmt_item32_t] { + slice::from_ref(&self.0) + } +} + impl Signal for [rmt_item32_t] { fn as_slice(&self) -> &[rmt_item32_t] { self @@ -847,13 +868,7 @@ impl FixedLengthSignal { .get_mut(index) .ok_or_else(|| EspError::from(ERR_ERANGE).unwrap())?; - // SAFETY: We're overriding all 32 bits, so it doesn't matter what was here before. - let inner = unsafe { &mut item.__bindgen_anon_1.__bindgen_anon_1 }; - inner.set_level0(pair.0.pin_state as u32); - inner.set_duration0(pair.0.ticks.0 as u32); - inner.set_level1(pair.1.pin_state as u32); - inner.set_duration1(pair.1.ticks.0 as u32); - + Symbol(*item).update(pair.0, pair.1); Ok(()) } }