Skip to content

Commit f3fd1f3

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

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed
74.4 KB
Binary file not shown.
+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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: &[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+
#[allow(dead_code)]
76+
const PLL_SYS_16MHZ: PLLConfig<Megahertz> = PLLConfig {
77+
vco_freq: Megahertz(480),
78+
refdiv: 1,
79+
post_div1: 6,
80+
post_div2: 5,
81+
};
82+
83+
/// Initialize system clocks and PLLs according to specified configs
84+
#[allow(clippy::too_many_arguments)]
85+
fn init_clocks_and_plls_cfg(
86+
xosc_crystal_freq: u32,
87+
xosc_dev: pac::XOSC,
88+
clocks_dev: pac::CLOCKS,
89+
pll_sys_dev: pac::PLL_SYS,
90+
pll_usb_dev: pac::PLL_USB,
91+
pll_sys_cfg: PLLConfig<Megahertz>,
92+
pll_usb_cfg: PLLConfig<Megahertz>,
93+
resets: &mut pac::RESETS,
94+
watchdog: &mut Watchdog,
95+
) -> Result<ClocksManager, InitError> {
96+
let xosc = hal::xosc::setup_xosc_blocking(xosc_dev, xosc_crystal_freq.Hz())
97+
.map_err(InitError::XoscErr)?;
98+
99+
// Configure watchdog tick generation to tick over every microsecond
100+
watchdog.enable_tick_generation((xosc_crystal_freq / 1_000_000) as u8);
101+
102+
let mut clocks = ClocksManager::new(clocks_dev);
103+
104+
let pll_sys = hal::pll::setup_pll_blocking(
105+
pll_sys_dev,
106+
xosc.operating_frequency().into(),
107+
pll_sys_cfg,
108+
&mut clocks,
109+
resets,
110+
)
111+
.map_err(InitError::PllError)?;
112+
let pll_usb = hal::pll::setup_pll_blocking(
113+
pll_usb_dev,
114+
xosc.operating_frequency().into(),
115+
pll_usb_cfg,
116+
&mut clocks,
117+
resets,
118+
)
119+
.map_err(InitError::PllError)?;
120+
121+
clocks
122+
.init_default(&xosc, &pll_sys, &pll_usb)
123+
.map_err(InitError::ClockError)?;
124+
Ok(clocks)
125+
}
126+
127+
/// Entry point to our bare-metal application.
128+
///
129+
/// The `#[entry]` macro ensures the Cortex-M start-up code calls this function
130+
/// as soon as all global variables are initialised.
131+
///
132+
/// The function configures the RP2040 peripherals, then outputs an audio signal
133+
/// on GPIO0 in an infinite loop.
134+
#[entry]
135+
fn main() -> ! {
136+
// Grab our singleton objects
137+
let mut pac = pac::Peripherals::take().unwrap();
138+
let core = pac::CorePeripherals::take().unwrap();
139+
140+
// Set up the watchdog driver - needed by the clock setup code
141+
let mut watchdog = hal::Watchdog::new(pac.WATCHDOG);
142+
143+
// Set chip voltage to 0.95V, BOD to 0.516V
144+
pac.VREG_AND_CHIP_RESET
145+
.vreg
146+
.write(|w| unsafe { w.vsel().bits(0b1000).en().bit(true) });
147+
pac.VREG_AND_CHIP_RESET
148+
.bod
149+
.write(|w| unsafe { w.vsel().bits(0b0001) });
150+
pac.SYSCFG
151+
.mempowerdown
152+
.write(|w| w.usb().bit(false).sram5().bit(false));
153+
pac.CLOCKS.wake_en0.write(|w| {
154+
w.clk_adc_adc()
155+
.bit(false)
156+
.clk_peri_spi0()
157+
.bit(false)
158+
.clk_peri_spi1()
159+
.bit(false)
160+
.clk_rtc_rtc()
161+
.bit(false)
162+
.clk_sys_dma()
163+
.bit(false)
164+
.clk_sys_pll_usb()
165+
.bit(false)
166+
});
167+
168+
// Configure the clocks
169+
// Note that we choose a nonstandard system clock rate, so that we can closely
170+
// control the PWM cycles so that they're (close to) a multiple of the audio sample rate.
171+
let clocks = init_clocks_and_plls_cfg(
172+
rp_pico::XOSC_CRYSTAL_FREQ,
173+
pac.XOSC,
174+
pac.CLOCKS,
175+
pac.PLL_SYS,
176+
pac.PLL_USB,
177+
PLL_SYS_16MHZ,
178+
PLL_USB_48MHZ,
179+
&mut pac.RESETS,
180+
&mut watchdog,
181+
)
182+
.ok()
183+
.unwrap();
184+
185+
// The single-cycle I/O block controls our GPIO pins
186+
let sio = hal::Sio::new(pac.SIO);
187+
188+
// Set the pins up according to their function on this particular board
189+
let pins = rp_pico::Pins::new(
190+
pac.IO_BANK0,
191+
pac.PADS_BANK0,
192+
sio.gpio_bank0,
193+
&mut pac.RESETS,
194+
);
195+
196+
// The delay object lets us wait for specified amounts of time (in
197+
// milliseconds)
198+
let mut delay = cortex_m::delay::Delay::new(core.SYST, clocks.system_clock.freq().integer());
199+
200+
// Init PWMs
201+
let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
202+
203+
// Setup the LED pin
204+
let mut led_pin = pins.led.into_push_pull_output();
205+
206+
// Configure PWM0
207+
let mut pwm = pwm_slices.pwm0;
208+
pwm.default_config();
209+
210+
// 131,000,000 Hz divided by (top * div.int).
211+
//
212+
// fPWM = fSYS / ((TOP + 1) * (CSR_PH_CORRECT + 1) * (DIV_INT + (DIV_FRAC / 16)))
213+
//
214+
// 32kHz ~= 131,000,000 / ((4096 + 1) * 1 * 1)
215+
pwm.set_top(256);
216+
pwm.set_div_int(2);
217+
218+
pwm.enable_interrupt();
219+
pwm.enable();
220+
221+
// Output channel A on PWM0 to GPIO0
222+
pwm.channel_a.output_to(pins.gpio0);
223+
224+
unsafe {
225+
// Share the PWM with our interrupt routine.
226+
PWM = Some(pwm);
227+
228+
// Unmask the PWM_IRQ_WRAP interrupt so we start receiving events.
229+
pac::NVIC::unmask(pac::interrupt::PWM_IRQ_WRAP);
230+
}
231+
232+
// N.B: Note that this would be much more efficiently implemented by using a DMA controller
233+
// to continuously feed audio samples to the PWM straight from memory. The hardware
234+
// is set up in a way where a rollover interrupt from the PWM channel can trigger a DMA
235+
// request for the next byte (or u16) of memory.
236+
// So while this is a good illustration for driving an audio signal from PWM, use DMA instead
237+
// for a real project.
238+
239+
// Infinite loop, with LED on while audio is playing.
240+
loop {
241+
// let _ = led_pin.set_high();
242+
243+
for i in AUDIO {
244+
// Rescale from signed i8 numbers to 0..4096 (the TOP register we specified earlier)
245+
//
246+
// The PWM channel will increment an internal counter register, and if the counter is
247+
// above or equal to this number, the PWM will output a logic high signal.
248+
let i = (*i as u16).wrapping_add(128) & 0xFF;
249+
250+
cortex_m::interrupt::free(|_| {
251+
// SAFETY: Interrupt cannot currently use this while we're in a critical section.
252+
let channel = &mut unsafe { PWM.as_mut() }.unwrap().channel_a;
253+
channel.set_duty(i);
254+
});
255+
256+
// Throttle until the PWM channel delivers us an interrupt saying it's done
257+
// with this cycle (the internal counter wrapped). The interrupt handler will
258+
// clear the interrupt and we'll send out the next sample.
259+
cortex_m::asm::wfi();
260+
}
261+
262+
// Flash the LED to let the user know that the audio is looping.
263+
// let _ = led_pin.set_low();
264+
delay.delay_ms(50);
265+
}
266+
}
267+
268+
#[interrupt]
269+
fn PWM_IRQ_WRAP() {
270+
// SAFETY: This is not used outside of interrupt critical sections in the main thread.
271+
let pwm = unsafe { PWM.as_mut() }.unwrap();
272+
273+
// Clear the interrupt (so we don't immediately re-enter this routine)
274+
pwm.clear_interrupt();
275+
}
276+
277+
// End of file

0 commit comments

Comments
 (0)