Skip to content

Commit cda2415

Browse files
committed
Add an example of using PWMs to generate an audio output
1 parent 9bce594 commit cda2415

File tree

2 files changed

+244
-0
lines changed

2 files changed

+244
-0
lines changed
74.4 KB
Binary file not shown.
+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
//! # Pico PWM Audio Example
2+
//!
3+
//! Drives GPIO0 with PWM to generate an audio signal for use with a speaker.
4+
//!
5+
//! Note that you will need to supply your own speaker. When hooked up to GPIO0,
6+
//! you should hear an audible chime.
7+
//!
8+
//! See the `Cargo.toml` file for Copyright and license details.
9+
10+
#![no_std]
11+
#![no_main]
12+
13+
use embedded_hal::digital::v2::OutputPin;
14+
use hal::{
15+
clocks::{ClocksManager, InitError},
16+
pac::interrupt,
17+
pll::{common_configs::PLL_USB_48MHZ, PLLConfig},
18+
pwm::{FreeRunning, Pwm0},
19+
Watchdog,
20+
};
21+
22+
// The macro for our start-up function
23+
use rp_pico::entry;
24+
25+
// GPIO traits
26+
use embedded_hal::PwmPin;
27+
28+
// Time handling traits
29+
use embedded_time::rate::*;
30+
31+
// Ensure we halt the program on panic (if we don't mention this crate it won't
32+
// be linked)
33+
use panic_halt as _;
34+
35+
// Pull in any important traits
36+
use rp_pico::hal::prelude::*;
37+
38+
// A shorter alias for the Peripheral Access Crate, which provides low-level
39+
// register access
40+
use rp_pico::hal::pac;
41+
42+
// A shorter alias for the Hardware Abstraction Layer, which provides
43+
// higher-level drivers.
44+
use rp_pico::hal;
45+
46+
/// Signed 8-bit raw PCM samples
47+
///
48+
/// If you want to create your own, use Audacity to create a recording with a
49+
/// sample rate of 32,000 Hz and then export it as raw 8-bit signed PCM with no
50+
/// file header.
51+
const AUDIO: &'static [u8] = include_bytes!("pico_pwm_audio.raw");
52+
53+
/// The hardware PWM driver that is shared with the interrupt routine.
54+
static mut PWM: Option<hal::pwm::Slice<Pwm0, FreeRunning>> = None;
55+
56+
// Output from vocalc.py
57+
/// This clock rate is closest to 176,400,000 Hz, which is a multiple of 44,100 Hz.
58+
#[allow(dead_code)]
59+
const PLL_SYS_176MHZ: PLLConfig<Megahertz> = PLLConfig {
60+
vco_freq: Megahertz(528),
61+
refdiv: 1,
62+
post_div1: 3,
63+
post_div2: 1,
64+
};
65+
66+
/// This clock rate is closest to 131,072,000 Hz, which is a multiple of 32,000 Hz (the audio sample rate).
67+
#[allow(dead_code)]
68+
const PLL_SYS_131MHZ: PLLConfig<Megahertz> = PLLConfig {
69+
vco_freq: Megahertz(1572),
70+
refdiv: 1,
71+
post_div1: 6,
72+
post_div2: 2,
73+
};
74+
75+
/// Initialize system clocks and PLLs according to specified configs
76+
#[allow(clippy::too_many_arguments)]
77+
fn init_clocks_and_plls_cfg(
78+
xosc_crystal_freq: u32,
79+
xosc_dev: pac::XOSC,
80+
clocks_dev: pac::CLOCKS,
81+
pll_sys_dev: pac::PLL_SYS,
82+
pll_usb_dev: pac::PLL_USB,
83+
pll_sys_cfg: PLLConfig<Megahertz>,
84+
pll_usb_cfg: PLLConfig<Megahertz>,
85+
resets: &mut pac::RESETS,
86+
watchdog: &mut Watchdog,
87+
) -> Result<ClocksManager, InitError> {
88+
let xosc = hal::xosc::setup_xosc_blocking(xosc_dev, xosc_crystal_freq.Hz())
89+
.map_err(InitError::XoscErr)?;
90+
91+
// Configure watchdog tick generation to tick over every microsecond
92+
watchdog.enable_tick_generation((xosc_crystal_freq / 1_000_000) as u8);
93+
94+
let mut clocks = ClocksManager::new(clocks_dev);
95+
96+
let pll_sys = hal::pll::setup_pll_blocking(
97+
pll_sys_dev,
98+
xosc.operating_frequency().into(),
99+
pll_sys_cfg,
100+
&mut clocks,
101+
resets,
102+
)
103+
.map_err(InitError::PllError)?;
104+
let pll_usb = hal::pll::setup_pll_blocking(
105+
pll_usb_dev,
106+
xosc.operating_frequency().into(),
107+
pll_usb_cfg,
108+
&mut clocks,
109+
resets,
110+
)
111+
.map_err(InitError::PllError)?;
112+
113+
clocks
114+
.init_default(&xosc, &pll_sys, &pll_usb)
115+
.map_err(InitError::ClockError)?;
116+
Ok(clocks)
117+
}
118+
119+
/// Entry point to our bare-metal application.
120+
///
121+
/// The `#[entry]` macro ensures the Cortex-M start-up code calls this function
122+
/// as soon as all global variables are initialised.
123+
///
124+
/// The function configures the RP2040 peripherals, then outputs an audio signal
125+
/// on GPIO0 in an infinite loop.
126+
#[entry]
127+
fn main() -> ! {
128+
// Grab our singleton objects
129+
let mut pac = pac::Peripherals::take().unwrap();
130+
let core = pac::CorePeripherals::take().unwrap();
131+
132+
// Set up the watchdog driver - needed by the clock setup code
133+
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
134+
135+
// Configure the clocks
136+
// Note that we choose a nonstandard system clock rate, so that we can closely
137+
// control the PWM cycles so that they're (close to) a multiple of the audio sample rate.
138+
let clocks = init_clocks_and_plls_cfg(
139+
rp_pico::XOSC_CRYSTAL_FREQ,
140+
pac.XOSC,
141+
pac.CLOCKS,
142+
pac.PLL_SYS,
143+
pac.PLL_USB,
144+
PLL_SYS_131MHZ,
145+
PLL_USB_48MHZ,
146+
&mut pac.RESETS,
147+
&mut watchdog,
148+
)
149+
.ok()
150+
.unwrap();
151+
152+
// The single-cycle I/O block controls our GPIO pins
153+
let sio = hal::Sio::new(pac.SIO);
154+
155+
// Set the pins up according to their function on this particular board
156+
let pins = rp_pico::Pins::new(
157+
pac.IO_BANK0,
158+
pac.PADS_BANK0,
159+
sio.gpio_bank0,
160+
&mut pac.RESETS,
161+
);
162+
163+
// The delay object lets us wait for specified amounts of time (in
164+
// milliseconds)
165+
let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().integer());
166+
167+
// Init PWMs
168+
let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
169+
170+
// Setup the LED pin
171+
let mut led_pin = pins.led.into_push_pull_output();
172+
173+
// Configure PWM0
174+
let mut pwm = pwm_slices.pwm0;
175+
pwm.default_config();
176+
177+
// 131,000,000 Hz divided by (top * div.int).
178+
//
179+
// fPWM = fSYS / ((TOP + 1) * (CSR_PH_CORRECT + 1) * (DIV_INT + (DIV_FRAC / 16)))
180+
//
181+
// 32kHz ~= 131,000,000 / ((4096 + 1) * 1 * 1)
182+
pwm.set_top(4096);
183+
pwm.set_div_int(1);
184+
185+
pwm.enable_interrupt();
186+
pwm.enable();
187+
188+
// Output channel A on PWM0 to GPIO0
189+
pwm.channel_a.output_to(pins.gpio0);
190+
191+
unsafe {
192+
// Share the PWM with our interrupt routine.
193+
PWM = Some(pwm);
194+
195+
// Unmask the PWM_IRQ_WRAP interrupt so we start receiving events.
196+
pac::NVIC::unmask(pac::interrupt::PWM_IRQ_WRAP);
197+
}
198+
199+
// N.B: Note that this would be much more efficiently implemented by using a DMA controller
200+
// to continuously feed audio samples to the PWM straight from memory. The hardware
201+
// is set up in a way where a rollover interrupt from the PWM channel can trigger a DMA
202+
// request for the next byte (or u16) of memory.
203+
// So while this is a good illustration for driving an audio signal from PWM, use DMA instead
204+
// for a real project.
205+
206+
// Infinite loop, with LED on while audio is playing.
207+
loop {
208+
let _ = led_pin.set_high();
209+
210+
for i in AUDIO {
211+
// Rescale from signed i8 numbers to 0..4096 (the TOP register we specified earlier)
212+
//
213+
// The PWM channel will increment an internal counter register, and if the counter is
214+
// above or equal to this number, the PWM will output a logic high signal.
215+
let i = ((*i as u16) << 4).wrapping_add(2048) & 0xFFF;
216+
217+
cortex_m::interrupt::free(|_| {
218+
// SAFETY: Interrupt cannot currently use this while we're in a critical section.
219+
let channel = &mut unsafe { PWM.as_mut() }.unwrap().channel_a;
220+
channel.set_duty(i);
221+
});
222+
223+
// Throttle until the PWM channel delivers us an interrupt saying it's done
224+
// with this cycle (the internal counter wrapped). The interrupt handler will
225+
// clear the interrupt and we'll send out the next sample.
226+
cortex_m::asm::wfi();
227+
}
228+
229+
// Flash the LED to let the user know that the audio is looping.
230+
let _ = led_pin.set_low();
231+
delay.delay_ms(50);
232+
}
233+
}
234+
235+
#[interrupt]
236+
fn PWM_IRQ_WRAP() {
237+
// SAFETY: This is not used outside of interrupt critical sections in the main thread.
238+
let pwm = unsafe { PWM.as_mut() }.unwrap();
239+
240+
// Clear the interrupt (so we don't immediately re-enter this routine)
241+
pwm.clear_interrupt();
242+
}
243+
244+
// End of file

0 commit comments

Comments
 (0)