Respberry Pi Pico W Generating Tones With Programmable I/O (PIO) Using MicroPython

by Trevor Lee in Circuits > Microcontrollers

1418 Views, 0 Favorites, 0 Comments

Respberry Pi Pico W Generating Tones With Programmable I/O (PIO) Using MicroPython

poster.png

In this post, I will show my Raspberry Pi Pico W Programmable I/O (PIO) MicroPython experiments -- generating tones with an attached speaker

First, I will show a simple MicroPython program that plays the tones Do-Re-Mi using PIO.

Then I will show a UI for playing tones using a virtual remote display on your Android phone -- DumbDisplay

The UI is in fact an extension of my previous similar UI implemented with the Arduino framework -- Raspberry Pi Pico playing song melody tones, with DumbDisplay control and keyboard input

Since this time the UI is implemented with MicroPython, the DumbDisplay MicroPython library is used instead. The setup for using DumbDisplay will be as described by the video -- Introducing DumbDisplay MicroPython Library -- with ESP32, Raspberry Pi Pico, and Raspberry Pi Zero

The Simple Idea

connect.png

The idea presented here is simple -- just like turning an LED on and off

Here I assume a speaker is connected to your Raspberry Pi Pico -- red wire of speaker to pin 5 of Pico, black wire of speaker to GND of Pico

import machine
speaker = machine.Pin(5, Pin.OUT)
while True:
speaker.on()
speaker.off()

This will generate a stream of square waves. Assuming that each on() / off() takes 10 clock cycles, with a clock frequency of 125 MHz, the generated square waves will have a frequency of 6250000 Hz, which I believe is too high for any human to hear.

To make it hearable like Do-Re-Mi tones, the key is to control how long it stays ON / OFF (half of each square wave).

For more precise control of timing, I believe RP2040's Programmable I/O (PIO) is the way to go.

Here is an equivalent MicroPython script that uses PIO to turn the speaker ON / OFF

import rp2
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pin_onoff():
    wrap_target()
    set(pins, 1)   # high
    set(pins, 0)   # low
    wrap()
sm = rp2.StateMachine(0, pin_onoff, set_base=Pin(5))
sm.active(1)
while True:
    pass

This will generate an even higher frequency tone, since each ON / OFF is only a single cycle.

Nevertheless, by just setting the freq parameter of StateMachine to 4000

import rp2
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pin_onoff():
    wrap_target()
    set(pins, 1)   # high
    set(pins, 0)   # low
    wrap()
sm = rp2.StateMachine(0, pin_onoff, freq=4000, set_base=Pin(5))
sm.active(1)
while True:
    pass

it should generate a tone that is hearable by humans -- 2000Hz

The Program That Sounds Do-Re-Mi With PIO

The previously described idea should work for my purpose. Nevertheless, the actual programming will be somewhat messier

import machine
from machine import Pin
@rp2.asm_pio(
    set_init=rp2.PIO.OUT_LOW,
    in_shiftdir=rp2.PIO.SHIFT_LEFT,
    out_shiftdir=rp2.PIO.SHIFT_LEFT,
)
def wave_prog():
    pull(block)
    mov(x, osr)     # waveCount
    pull(block)
    label("loop")
    mov(y, osr)     # halfWaveNumCycles
    set(pins, 1)   # high
    label("high")
    jmp(y_dec, "high")
    mov(y, osr)     # halfWaveNumCycles
    set(pins, 0)   # low
    label("low")
    jmp(y_dec, "low")
    jmp(x_dec, "loop")
# the clock frequency of Raspberry Pi Pico is 125MHz; 1953125 is 125MHz / 64
sm = rp2.StateMachine(0, wave_prog, freq=1953125, set_base=Pin(5))
sm.active(1)
def HWPlayTone(freq: int, duration: int):
  # count 1 cycle for jmp() ==> 1 cycle per half wave ==> 2 cycles per wave
    halfWaveNumCycles = round(1953125.0 / freq / 2)
    waveCount = round(duration * freq / 1000.0)
    sm.put(waveCount)
    sm.put(halfWaveNumCycles)

import time
HWPlayTone(262, 1000) # Do
time.sleep(1)
HWPlayTone(294, 1000) # Re
time.sleep(1)
HWPlayTone(330, 1000) # Mi
time.sleep(1)
  • the state machine's frequency is set to 1,953,125, which is 125,000,000 divided by 64; note that the clock frequency of Raspberry Pi Pico is 125MHz
  • since the state machine's frequency is fixed, in order to play different tones, the timing need to be handled by the state machine
  • the "caller" function is HWPlayTone(), which accepts two parameters -- freq and duration -- for the tone's frequency and duration (in milliseconds)
  • two values will be provided to the state machine when it is trigged to generate a tone -- halfWaveNumCycles and waveCount
  • these two values are calculated according to the tone's frequency and duration when HWPlayTone() is called
  • the state machine will initially block waiting for the two values
  • the first value will be waveCount; and it will be stored in the X register
  • the second value will be halfWaveNumCycles; it will be stored in the Y register
  • note that the second value will be kept in the "out shift" register until the next tone is triggered; assuming tones are played one by one, the "out shift" register value will be the same throughout a tone
  • the X and Y registers are used to control looping turning the speaker ON / OFF
  • jmp(y_dec, "high") will check if the Y register already reached 0, if not, decrement it and jump to the label "high"; that is the "high" half of a square wave
  • jmp(y_dec, "how") will check if the Y register already reached 0, if not, decrement it and jump to the label "low"; that is the "low" half of a square wave
  • finally, jmp(x_dec, "loop") will check if the X register already reached 0, if not, decrement it and jump to the label "loop"; which controls how many square waves are to be played

I guess if the above PIO program is to be translated to Python syntax, it would look something like

speaker = machine.Pin(5, Pin.OUT)
def wave_prog():
osr = shiftFIFO(block=True)
x = osr
osr = shiftFIFO(block=True)
while x > 0:
y = osr
speaker.on()
while y > 0:
  y = y - 1
y = osr
speaker.off()
while y > 0:
  y = y - 1
x = x - 1

UI for the Play Tone Experiment, With DumbDisplay

git_clone.png
thonny_01.png
thonny_02.png
thonny_03.png
thonny_04.png
thonny_05.png

All along I have been assuming that you will be using Thonny for developing MicroPython programs for your Raspberry Pi Pico W. Indeed, the steps described next assume that Thonny is connected to your Raspberry Pi Pico, for running the MicroPython UI of the experiment.

Assuming you have MicroPython installed to your Raspberry Pi Pico W, like with Thonny's support of downloading MicroPython firmware, the next big step is to install DumbDisplay MicroPython library to your Raspberry Pi Pico W.

One easy way is to clone the DumbDisplay MicroPython library from its GitHub repository by running

git clone https://github.com/trevorwslee/MicroPython-DumbDisplay.git

This should clone the repository files to the directory MicroPython-DumbDisplay

BTW, if you have previously cloned the DumbDisplay MicroPython Github repo before, to refresh the local clone MicroPython-DumbDisplay, cd into MicroPython-DumbDisplay and run

git pull origin master

After cloning, open Thonny and navigate into the directory MicroPython-DumbDisplay, you should see the directory dumbdisplay, which is the source of the DumbDisplay MicroPython library.

Copy the whole directory to your Raspberry Pi Pico W.

This should "install" the needed DumbDisplay library to your Raspberry Pi Pico for MicroPython programs that use the DumbDisplay MicroPython library.

Since the UI MicroPython program here will be using WIFI to connect to your Android DumbDisplay app, you will also need to copy the file _my_secret.py to your Raspberry Pi Pico W. The file_my_secret.py is supposed to store your SSID and password for DumbDisplay library to connect to your WIFI router, and it will be called for by the MicroPython UI program

WIFI_SSID="your wifi router ssid"
WIFI_PWD="your wifi router password"

After copying _my_secret.py, be sure to modify the copy there in your Raspberry Pi Pico W for your actual credentials. Note that it will be the copy in your Raspberry Pi Pico W that will be called for, since it is your Raspberry Pi Pico that runs the UI program.

Since the MicroPython UI program for this experiment should already bundled with the DumbDisplay MicroPython library, you can simply open MicroPython-DumbDisplay/sample/melody/main.py and run it with your Raspberry Pi Pico.

Note that you do not need to copy MicroPython-DumbDisplay/sample/melody/main.py to your Raspberry Pi Pico W, but if you choose to, you can do so. If you copy MicroPython-DumbDisplay/sample/melody/main.py to your Raspberry Pi Pico W, every time you boot up your Raspberry Pi Pico W, the program will run.

When the program is run you should see from Thonny console lines like

connecting WIFI ... <wifi router> ...
... connected WIFI
connecting socket ... listing on 192.168.0.46:10201 ...

The MicroPython program should work with other microcontroller boards like ESP32, even with regular Python like with Raspberry Pi Zero W. But of course, only RP2040 microcontroller like Raspberry Pi Pico W is expected to support PIO to drive speaker for playing tone the way like in this experiment.

Making Connection

dd_00.jpg
dd_01.jpg
dd_02.jpg
dd_03.jpg
dd_04.jpg
dd_05.jpg

Open your Android phone's DumbDisplay Android app, and make connection to the IP address shown in Thonny's console (as described previously).

After connecting your Raspberry Pi Pico W from your Android Phone DumbDisplay app, you should see the UI presented on your Android Phone.

The UI

dd_05.jpg

At the bottom is a simple virtual keyboard that you can press to have the corresponding tone played. By default, the tones will be played with the speaker of your phone.

At the top are three buttons -- ⏯, ⏮, and 📢

With the 📢 button, you can switch to use the speaker attached to your Raspberry Pi Pico W for the tones.

By pressing the ⏯, you play the tones of my favorite song -- Amazing Grace. You can restart the song playing by pressing ⏮

That is it.

Enjoy!

Respberry Pi Pico W Generating Tones With Programmable I/O (PIO) Using MicroPython

Enjoy!

Peace be with you! May God bless you! Jesus loves you! Amazing Grace!