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

Conversation

DrChat
Copy link

@DrChat DrChat commented Jun 12, 2022

This PR merely adds a basic example of using the PWM controller to output an audio signal at 32kHz on GPIO0.

One of the more unique parts of this example is configuring the system clock to a nonstandard rate that is a multiple of the audio sample rate, so that the PWM clock can be divided by an integral number to deliver the audio samples.

@DrChat DrChat force-pushed the feature/pwm_audio_example branch 3 times, most recently from f3fd1f3 to cda2415 Compare June 14, 2022 01:55
@DrChat DrChat force-pushed the feature/pwm_audio_example branch from cda2415 to d54217c Compare June 14, 2022 02:14
@paddywwoof
Copy link
Contributor

paddywwoof commented Jun 16, 2022

I tried this with a tiny speaker I had lying around here, and it seems to work fine.

@jannic
Copy link
Member

jannic commented Jun 16, 2022

I wonder if it would be possible to avoid the binary .raw file. While it's not huge, it would be the largest file in the repo, and it's not obvious what it contains.
Perhaps the audio samples could be generated instead of including them as a binary?

@paddywwoof
Copy link
Contributor

Yes it would be very easy to synthesize the sound but I suppose @DrChat wants to demonstrate the ease of including sound files into the program. At least half the sample is empty, presumably to improve the impression when repeated (though that could be done just with a delay). If the blank parts are removed it would be 31.6k

The other thing is that it might be better using a WAV format file which would obviously be a sound sample and would have the advantage that it could be played by clicking on it in most file browsers. I can't find an option in Audactiy to save WAV as signed 8-bit so to do that the instructions would have to be

/// 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 file coding Unsigned 8-bit PCM
const AUDIO: &[u8] = include_bytes!("pico_pwm_audio.wav");

and later

        for i in &AUDIO[44..] {
...
            let i = ((*i as u16) << 3).wrapping_add(2048) & 0xFFF;

@DrChat
Copy link
Author

DrChat commented Jun 17, 2022

Oh, huh that is clever - I didn't know you could just skip the wav header and directly play audio samples.
Let me play around with that, maybe I could even pull in a no_std wav parser and give it some flexibility.
Also, unsigned 8-bit PCM samples would actually be better since I'm already rescaling the sound from signed numbers.

As for the file size, I suppose technically it would be best to check it in as a git LFS file as that's the best practice for binary files.

And yeah, I figure it'll allow more creativity from users if the sample included an easy-to-replace audio file rather than a synthesized sound.

//! 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.

// 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();
    }
}

@jannic
Copy link
Member

jannic commented Jun 17, 2022

As my previous comments may sound somewhat negative: I really like the idea of an example outputting sounds! Just blinking LEDs gets boring. Bonus points for simple hardware (ie. not needing external i2s hardware or similar).

@WeirdConstructor
Copy link
Contributor

WeirdConstructor commented Jun 17, 2022

Not too long ago I built this, it's two simple RC filters in a row and some code to generate a saw tooth:
image

That code can be found here, there is also a sine wave example: https://github.com/WeirdConstructor/RustRP2040Code/tree/master/pwm_dac_saw_sampling/src

Also: I put my headphones directly to that output and got a clearly audible sound at the right pitch. Of course, adding an op amp stage would be better to amplify the signal. But for experimentation this is alright.

@paddywwoof
Copy link
Contributor

paddywwoof commented Jun 17, 2022

@WeirdConstructor that looks a tidy example. I see on your repo you have switched from the delay_us() system in your screenshot to nb::block!(cnt_down.wait()) which might be the way for @DrChat to go (though it probably removes the need for the clever clock setting section!)

It reminded me of something I did ages ago with at atmega32 https://www.youtube.com/watch?v=HrDFpDNN2cw I'm not sure where the code is for that now but I think I avoided floats by using lookup tables, based on this sketch by Adrian Freed

@9names
Copy link
Member

9names commented Jul 10, 2022

A reminder for who-ever merges this: be sure to squash to avoid both audio files ending up in the repo history.

@9names
Copy link
Member

9names commented Aug 6, 2022

Section 3.1.4 of Hardware Design with RP2040 outlines a good circuit for getting line-out audio with PWM.
https://github.com/rgrosset/pico-pwm-audio has a circuit that is simple enough to breadboard, which feels appropriate for the level of this example. At the very least, the fact that it has some DC blocking would make me more comfortable.

It also has a C project that uses the same system-frequency-matched PWM style that this example does, but they also switch the power supply from PFM to PWM mode to reduce switching noise (at the cost of increased power consumption).
It's only a single GPIO write, should we do that?

@DrChat
Copy link
Author

DrChat commented Oct 11, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants