Skip to content

Commit

Permalink
RMT symbols and musical buzzer example bugfix for ESP32 (#354)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
dsheets authored Jan 31, 2024
1 parent e837081 commit 5657073
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 56 deletions.
93 changes: 64 additions & 29 deletions examples/rmt_musical_buzzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand All @@ -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 {
Expand All @@ -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<Self::Item> {
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);
Expand All @@ -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 {
Expand Down
69 changes: 42 additions & 27 deletions src/rmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -641,7 +626,7 @@ impl<'d> TxRmtDriver<'d> {
#[cfg(feature = "alloc")]
pub fn start_iter<T>(&mut self, iter: T) -> Result<(), EspError>
where
T: Iterator<Item = rmt_item32_t> + Send + 'static,
T: Iterator<Item = Symbol> + Send + 'static,
{
let iter = alloc::boxed::Box::new(UnsafeCell::new(iter));
unsafe {
Expand Down Expand Up @@ -675,7 +660,7 @@ impl<'d> TxRmtDriver<'d> {

pub fn start_iter_blocking<T>(&mut self, iter: T) -> Result<(), EspError>
where
T: Iterator<Item = rmt_item32_t> + Send,
T: Iterator<Item = Symbol> + Send,
{
let iter = UnsafeCell::new(iter);
unsafe {
Expand Down Expand Up @@ -719,7 +704,7 @@ impl<'d> TxRmtDriver<'d> {
translated_size: *mut usize,
item_num: *mut usize,
) where
T: Iterator<Item = rmt_item32_t>,
T: Iterator<Item = Symbol>,
{
// An `UnsafeCell` is needed here because we're casting a `*const` to a `*mut`.
// Safe because this is the only existing reference.
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -847,13 +868,7 @@ impl<const N: usize> FixedLengthSignal<N> {
.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(())
}
}
Expand Down

0 comments on commit 5657073

Please sign in to comment.