Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an example of using PWMs to generate an audio output #356

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions boards/rp-pico/examples/pico_pwm_audio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//! # Pico PWM Audio Example
//!
//! Drives GPIO0 with PWM to generate an audio signal for use with a speaker.
//!
//! Note that you will need to supply your own speaker. When hooked up to GPIO0,
//! you should hear an audible chime.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, but I assume it's not a good idea to connect some random speaker directly to the GPIO pin. It may work for some speakers with proper specs, but for most, some kind of amplifier or at least a resistor should be inserted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I'm sure it's not good, especially something that might have inductance. I found a 2N2222 transistor with resistors already soldered from another project (tiny motor from arduino I think) and that makes it a bit louder (a bad thing after only a few seconds!) and safer. But it does add some external hardware. Maybe just mention that a transistor and resistors would be a good idea.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that may be a good point! I hope I'm not damaging my Pi by wiring a speaker straight up to the GPIO pin haha.
What would be a better wording? My knowledge about software engineering is certainly better than my knowledge of electrical engineering.

Copy link
Member

@9names 9names Jun 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend skipping the transistor and resistor recommendations and jump straight to recommending a mono audio amplifier. You can pick one up for a couple of dollars on ebay, it will sound a lot better, have adjustable volume, and folks are far less likely to destroy their microcontroller that way.
Note that you still need to condition the signal in this case - audio amps aren't expecting this much voltage either.

//!
//! See the `Cargo.toml` file for Copyright and license details.

#![no_std]
#![no_main]

use embedded_hal::digital::v2::OutputPin;
use hal::{
clocks::{ClocksManager, InitError},
pac::interrupt,
pll::{common_configs::PLL_USB_48MHZ, PLLConfig},
pwm::{FreeRunning, Pwm0},
Watchdog,
};

// The macro for our start-up function
use rp_pico::entry;

// GPIO traits
use embedded_hal::PwmPin;

// Time handling traits
use embedded_time::rate::*;

// Ensure we halt the program on panic (if we don't mention this crate it won't
// be linked)
use panic_halt as _;

// Pull in any important traits
use rp_pico::hal::prelude::*;

// A shorter alias for the Peripheral Access Crate, which provides low-level
// register access
use rp_pico::hal::pac;

// A shorter alias for the Hardware Abstraction Layer, which provides
// higher-level drivers.
use rp_pico::hal;

/// Unsigned 8-bit PCM samples (in a WAV container)
///
/// If you want to create your own, use Audacity to create a recording with a
/// sample rate of 32,000 Hz and then export it as WAV 8-bit unsigned PCM.
const AUDIO: &[u8] = include_bytes!("pico_pwm_audio.wav");

/// The hardware PWM driver that is shared with the interrupt routine.
static mut PWM: Option<hal::pwm::Slice<Pwm0, FreeRunning>> = None;

// Output from vocalc.py
/// This clock rate is closest to 176,400,000 Hz, which is a multiple of 44,100 Hz.
#[allow(dead_code)]
const PLL_SYS_176MHZ: PLLConfig<Megahertz> = PLLConfig {
vco_freq: Megahertz(528),
refdiv: 1,
post_div1: 3,
post_div2: 1,
};

/// This clock rate is closest to 131,072,000 Hz, which is a multiple of 32,000 Hz (the audio sample rate).
#[allow(dead_code)]
const PLL_SYS_131MHZ: PLLConfig<Megahertz> = PLLConfig {
vco_freq: Megahertz(1572),
refdiv: 1,
post_div1: 6,
post_div2: 2,
};

/// Initialize system clocks and PLLs according to specified configs
#[allow(clippy::too_many_arguments)]
fn init_clocks_and_plls_cfg(
xosc_crystal_freq: u32,
xosc_dev: pac::XOSC,
clocks_dev: pac::CLOCKS,
pll_sys_dev: pac::PLL_SYS,
pll_usb_dev: pac::PLL_USB,
pll_sys_cfg: PLLConfig<Megahertz>,
pll_usb_cfg: PLLConfig<Megahertz>,
resets: &mut pac::RESETS,
watchdog: &mut Watchdog,
) -> Result<ClocksManager, InitError> {
let xosc = hal::xosc::setup_xosc_blocking(xosc_dev, xosc_crystal_freq.Hz())
.map_err(InitError::XoscErr)?;

// Configure watchdog tick generation to tick over every microsecond
watchdog.enable_tick_generation((xosc_crystal_freq / 1_000_000) as u8);

let mut clocks = ClocksManager::new(clocks_dev);

let pll_sys = hal::pll::setup_pll_blocking(
pll_sys_dev,
xosc.operating_frequency().into(),
pll_sys_cfg,
&mut clocks,
resets,
)
.map_err(InitError::PllError)?;
let pll_usb = hal::pll::setup_pll_blocking(
pll_usb_dev,
xosc.operating_frequency().into(),
pll_usb_cfg,
&mut clocks,
resets,
)
.map_err(InitError::PllError)?;

clocks
.init_default(&xosc, &pll_sys, &pll_usb)
.map_err(InitError::ClockError)?;
Ok(clocks)
}

/// Entry point to our bare-metal application.
///
/// The `#[entry]` macro ensures the Cortex-M start-up code calls this function
/// as soon as all global variables are initialised.
///
/// The function configures the RP2040 peripherals, then outputs an audio signal
/// on GPIO0 in an infinite loop.
#[entry]
fn main() -> ! {
// Grab our singleton objects
let mut pac = pac::Peripherals::take().unwrap();
let core = pac::CorePeripherals::take().unwrap();

// Set up the watchdog driver - needed by the clock setup code
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);

// Configure the clocks
// Note that we choose a nonstandard system clock rate, so that we can closely
// control the PWM cycles so that they're (close to) a multiple of the audio sample rate.
let clocks = init_clocks_and_plls_cfg(
rp_pico::XOSC_CRYSTAL_FREQ,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
PLL_SYS_131MHZ,
PLL_USB_48MHZ,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();

// The single-cycle I/O block controls our GPIO pins
let sio = hal::Sio::new(pac.SIO);

// Set the pins up according to their function on this particular board
let pins = rp_pico::Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);

// The delay object lets us wait for specified amounts of time (in
// milliseconds)
let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().integer());

// Init PWMs
let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);

// Setup the LED pin
let mut led_pin = pins.led.into_push_pull_output();

// Configure PWM0
let mut pwm = pwm_slices.pwm0;
pwm.default_config();

// 131,000,000 Hz divided by (top * div.int).
//
// fPWM = fSYS / ((TOP + 1) * (CSR_PH_CORRECT + 1) * (DIV_INT + (DIV_FRAC / 16)))
//
// 32kHz ~= 131,000,000 / ((4096 + 1) * 1 * 1)
pwm.set_top(4096);
pwm.set_div_int(1);

pwm.enable_interrupt();
pwm.enable();

// Output channel A on PWM0 to GPIO0
pwm.channel_a.output_to(pins.gpio0);

unsafe {
// Share the PWM with our interrupt routine.
PWM = Some(pwm);

// Unmask the PWM_IRQ_WRAP interrupt so we start receiving events.
pac::NVIC::unmask(pac::interrupt::PWM_IRQ_WRAP);
}

// N.B: Note that this would be much more efficiently implemented by using a DMA controller
// to continuously feed audio samples to the PWM straight from memory. The hardware
// is set up in a way where a rollover interrupt from the PWM channel can trigger a DMA
// request for the next byte (or u16) of memory.
// So while this is a good illustration for driving an audio signal from PWM, use DMA instead
// for a real project.

// Infinite loop, with LED on while audio is playing.
loop {
let _ = led_pin.set_high();

// N.B: Skip the WAV header here. We're going to assume the format is what we expect.
for i in &AUDIO[0x2C..] {
// Rescale from unsigned u8 numbers to 0..4096 (the TOP register we specified earlier)
//
// The PWM channel will increment an internal counter register, and if the counter is
// above or equal to this number, the PWM will output a logic high signal.
let i = ((*i as u16) << 4) & 0xFFF;

cortex_m::interrupt::free(|_| {
// SAFETY: Interrupt cannot currently use this while we're in a critical section.
let channel = &mut unsafe { PWM.as_mut() }.unwrap().channel_a;
channel.set_duty(i);
});

// Throttle until the PWM channel delivers us an interrupt saying it's done
// with this cycle (the internal counter wrapped). The interrupt handler will
// clear the interrupt and we'll send out the next sample.
cortex_m::asm::wfi();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only works if the PWM interrupt is the only one enabled.
Even for an example, I'd prefer a way which can be composed with other functionality.
Also, see the documentation of WFI: "WFI is intended for power saving only. When writing software assume that WFI might behave as a NOP operation."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, projects playing sounds are likely to have other hardware inputs such as ADC, buttons etc. so not ideal if the example stops working when other functionality is introduced.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point. You guys think something like wfe and sev would be better instead?

Copy link
Contributor

@paddywwoof paddywwoof Jun 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach used in @WeirdConstructor's code https://github.com/WeirdConstructor/RustRP2040Code/blob/master/pwm_dac_saw_sampling/src/example_1000hz_saw.rs which I think is the same as the pwm_blinky example seems much simpler. However there might be disadvantages I'm unaware of.

EDIT The following appears to run OK but may lack some useful functionality of the interrupt controlled version.

#![no_std]
#![no_main]

use cortex_m::prelude::*;
use embedded_hal::digital::v2::OutputPin;
use embedded_time::duration::Extensions;
use panic_halt as _;
use rp_pico::entry;

const AUDIO: &[u8] = include_bytes!("pico_pwm_audio.wav");

#[entry]
fn main() -> ! {
    let mut pac = rp_pico::hal::pac::Peripherals::take().unwrap();
    let _core = rp_pico::hal::pac::CorePeripherals::take().unwrap();
    let mut watchdog = rp_pico::hal::Watchdog::new(pac.WATCHDOG);
    let _clocks = rp_pico::hal::clocks::init_clocks_and_plls(
        rp_pico::XOSC_CRYSTAL_FREQ,
        pac.XOSC,
        pac.CLOCKS,
        pac.PLL_SYS,
        pac.PLL_USB,
        &mut pac.RESETS,
        &mut watchdog,
    )
    .ok()
    .unwrap();

    let timer = rp_pico::hal::Timer::new(pac.TIMER, &mut pac.RESETS);
    let mut count_down = timer.count_down();
    let mut count_down_sample = timer.count_down();

    let sio = rp_pico::hal::Sio::new(pac.SIO);
    let pins = rp_pico::Pins::new(
        pac.IO_BANK0,
        pac.PADS_BANK0,
        sio.gpio_bank0,
        &mut pac.RESETS,
    );

    let mut led_pin = pins.led.into_push_pull_output();

    let pwm_slices = rp_pico::hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
    let mut pwm = pwm_slices.pwm0;
    pwm.default_config();
    pwm.set_top(256); // as unsigned 8-bit WAV
    pwm.set_div_int(1);
    pwm.enable();
    let channel = &mut pwm.channel_a;
    channel.output_to(pins.gpio0);

    loop {
        count_down_sample.start(2000.milliseconds());
        for i in &AUDIO[0x2C..] {
            count_down.start((31250_u32).nanoseconds());

            channel.set_duty(*i as u16);

            let _ = nb::block!(count_down.wait());
        }

        led_pin.set_high().unwrap();

        let _ = nb::block!(count_down_sample.wait());
        led_pin.set_low().unwrap();
    }
}

}

// Flash the LED to let the user know that the audio is looping.
let _ = led_pin.set_low();
delay.delay_ms(200);
}
}

#[interrupt]
fn PWM_IRQ_WRAP() {
// SAFETY: This is not used outside of interrupt critical sections in the main thread.
let pwm = unsafe { PWM.as_mut() }.unwrap();

// Clear the interrupt (so we don't immediately re-enter this routine)
pwm.clear_interrupt();
}

// End of file
Binary file added boards/rp-pico/examples/pico_pwm_audio.wav
Binary file not shown.