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
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
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
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
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
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!
Enjoy!
Peace be with you! May God bless you! Jesus loves you! Amazing Grace!