-
Notifications
You must be signed in to change notification settings - Fork 235
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
//! | ||
//! 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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only works if the PWM interrupt is the only one enabled. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good point. You guys think something like There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.