bandarra.me

Play music with a raspberry Pi Pico and Rust

If you are coming from Micropython or the Arduino IDE, playing a musical note is as straightforward as calling freq() passing the desired note frequency as a parameter, as in the MicroPython example below:

    import machine

    p12 = machine.Pin(12)
    pwm12 = machine.PWM(p12)
    pwm12.freq(440) # 440Hz is an A4 note.
    pwm12.duty(512)

However, when using the Raspberry Pi Pico C/C++ SDK or the Rust's rp-hal, there isn't a method to set the frequency directy and it is necessary to use a lower-level API, the pwm_config_set_wrap() on C/C++ or set_top in Rust.

In order to use this, understanding how PWM is implemented on the Pico is helpful. Here's what the C/C++ documentation says:

The PWM hardware functions by continuously comparing the input value to a free-running counter. This produces a toggling output where the amount of time spent at the high output level is proportional to the input value. The fraction of time spent at the high signal level is known as the duty cycle of the signal.

The default behaviour of a PWM slice is to count upward until the wrap value (pwm_config_set_wrap) is reached, and then immediately wrap to 0. PWM slices also offer a phase-correct mode, where the counter starts to count downward after reaching TOP, until it reaches 0 again.

The hardware PWM is implemented with a counter that, by default, is incremented at the same rate as the Pico crystal frequency, or 12Mhz, and input that is compared to that counter value, in order to set the voltage to high or low.

The input used for comparison is passed to the system using set_duty() and the maximum value for the counter is set via set_top(). In the example below, the counter is set to 1000, creating a frequency of 12Khz (12Mhz divided by 1000). In this case, we're setting the duty cycle to half the value of the counter - or a 50% duty cycle.

 pwm.set_top(1000);
 pwm.channel_b.set_duty(500);

By setting top to 1, a maximum frequency of 12Mhz can be created. The maximum value that can be passed to set_top() is 65535 (an u16), creating a frequency of 183Hz.

Since we are concerned about musical notes and the humans can detect sounds between 20Hz and 20Khz, which doesn't fully overlap with the available frequencies between 183Hz to 12Mhz.

This problem can be solved by setting a clock divider, via set_div_int() and set_div_frac(). This causes the counter to be updated at a lower frequency. When setting the divider to 2, for instance, the counter is only incremented every other cycle, decreasing the frequency of updates to 6Mhz (12Mhz / 2).

Setting the divider to 40, for instance, would set the maximum update frequency to 300Kh. Combined with set_top(), this allows a minimum frequency of 4.5Hz, which is below the minimum for the human hearing.

It is possible to calculate the top value for a particlular note by dividing the 12Mhz by the divider, then by the note frequency. Using a 40 as a divider and the an A4 note as an example, divide 12Mhz by 40, and the result by 440.0. The resulting top value is 681.

Below is an example of playing musical notes with rp-hal on the Rasperry Pi Pico:

    fn calc_note(freq: f32) -> u16 {
        (12_000_000 as f32 / 40 as f32 / freq) as u16
    }

    let pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);
    let mut buzzer = pwm_slices.pwm5;

    // Notes
    let c4 = calc_note(261.63);
    let d4 = calc_note(293.66);
    let e4 = calc_note(329.63);
    let f4 = calc_note(349.23);
    let g4 = calc_note(392.00);
    let a4 = calc_note(440.00);
    let b4 = calc_note(493.88);
    let space = calc_note(0.0);

    let doremi = [c4, d4, e4, f4, g4, a4, b4];

    let twinkle_twinkle = [
        c4, c4, g4, g4, a4, a4, g4, space,
        f4, f4, e4, e4, d4, d4, c4, space,
        g4, g4, f4, f4, e4, e4, d4, space,
        g4, g4, f4, f4, e4, e4, d4, space,
        c4, c4, g4, g4, a4, a4, g4, space,
        f4, f4, e4, e4, d4, d4, c4, space,
    ];

    for top in twinkle_twinkle {
        buzzer.channel_b.set_duty(top / 2); // 50% Duty Cycle
        buzzer.set_top(top);
        delay.start(500.milliseconds());
        let _ = nb::block!(delay.wait());

        buzzer.channel_b.set_duty(0);
        delay.start(100.milliseconds());
        let _ = nb::block!(delay.wait());
    }