In JavaScript and different languages, we name a stunning or inconsistent conduct a “Wat!” [that is, a “What!?”]. For instance, in JavaScript, an empty array plus an empty array produces an empty string, [] + [] === ""
. Wat!
Python, by comparability, is extra constant and predictable. Nonetheless, one nook of MicroPython on the Raspberry Pi Pico microcontroller affords related surprises. Particularly, the Pico’s Programmable Enter/Output (PIO) subsystem, whereas extremely highly effective and versatile, comes with peculiarities.
PIO programming issues as a result of it supplies an ingenious resolution to the problem of exact, low-level {hardware} management. It’s extremely quick — even when programmed from MicroPython, PIO performs simply as effectively because it does from Rust or C/C++. Its flexibility is unparalleled: moderately than counting on special-purpose {hardware} for the numerous peripherals you would possibly wish to management, PIO means that you can outline customized behaviors in software program, seamlessly adapting to your wants with out including {hardware} complexity.
Think about this straightforward instance: a $15 theremin-like musical instrument. By waving their hand within the air, the musician adjustments the pitch of (admittedly annoying) tones. Utilizing PIO supplies a easy strategy to program this machine that ensures it reacts immediately to motion.
So, all is great, besides — to paraphrase Spider-Man:
With nice energy comes… 9 Wats!?
We’ll discover and illustrate these 9 PIO Wats by means of the creation of this theremin.
Who Is This Article For?
- All Programmers: Microcontrollers just like the Pico value underneath $7 and help high-level languages like Python, Rust, and C/C++. This text will present how microcontrollers let your packages work together with the bodily world and introduce you to programming the Pico’s low-level, high-performance PIO {hardware}.
- MicroPython Pico Programmers: Curious in regards to the Pico’s hidden potential? Past its two essential cores, it has eight tiny “state machines” devoted to PIO programming. These state machines take over time-critical duties, releasing up the primary processors for different work and enabling stunning parallelism.
- Rust and C/C++ Pico Programmers: Whereas this text makes use of MicroPython, PIO programming is — for good and unhealthy — almost similar throughout all languages. For those who perceive it right here, you’ll be well-equipped to use it in Rust or C/C++.
- PIO Programmers: The journey by means of 9 Wats might not be as entertaining as JavaScript’s quirks (fortunately), however it’ll make clear the peculiarities of PIO programming. For those who’ve ever discovered PIO programming complicated, this text ought to reassure you that the issue isn’t (essentially) you — it’s partly PIO itself. Most significantly, understanding these Wats will make writing PIO code less complicated and simpler.
Lastly, this text isn’t about “fixing” PIO programming. PIO excels at its main goal: effectively and flexibly dealing with customized peripheral interfaces. Its design is purposeful and well-suited to its targets. As an alternative, this text focuses on understanding PIO programming and its quirks — beginning with a bonus Wat.
Regardless of their title, the eight “PIO state machines” within the Raspberry Pi Pico will not be state machines within the formal pc science sense. As an alternative, they’re tiny programmable processors with their very own assembly-like instruction set, able to looping, branching, and conditional operations. In actuality, they — like most sensible computer systems — are von Neumann machines.
Every state machine processes one instruction per clock cycle. The $4 Pico 1 runs at 125 million cycles per second, whereas the $5 Pico 2 affords a quicker 150 million cycles per second. Every instruction performs a easy operation, equivalent to “transfer a price” or “leap to a label”.
With that bonus Wat out of the way in which, let’s transfer to our first essential Wat.
In PIO programming, a register is a small, quick storage location that acts like a variable for the state machine. You would possibly dream of an abundance of variables to carry your counters, delays, and momentary values, however the actuality is brutal: you solely get two general-purpose registers, x
and y
. It is like The Starvation Video games, the place irrespective of what number of tributes enter the world, solely Katniss and Peeta emerge as victors. You’re pressured to winnow down your wants to suit inside these two registers, ruthlessly deciding what to prioritize and what to sacrifice. Additionally, just like the Starvation Video games, we will typically bend the principles.
Let’s begin with a problem: create a backup beeper — 1000 Hz for ½ second, silence for ½ second, repeat. The outcome? “Beep Beep Beep…”
We want 5 variables:
half_period:
The variety of clock cycles to carry the voltage excessive after which low to create a 1000 Hz tone. That is 150,000,000 / 1000 / 2 = 75,000 cycles excessive and 75,000 cycles low.
y
: Loop counter from 0 tohalf_period
to create a delay.period_count
: The variety of repeated durations wanted to fill ½ second of time. 150,000,000 × 0.5 / (75,000 × 2) = 500.x
: Loop counter from 0 toperiod_count
to fill ½ second of time.silence_cycles
: The variety of clock cycles for ½ second of silence. 150,000,000 × 0.5 = 75,000,000.
We wish 5 registers however can solely have two, so let the video games start! Could the chances be ever in your favor.
First, we will eradicate silence_cycles
as a result of it may be derived as half_period × period_count × 2
. Whereas PIO doesn’t help multiplication, it does help loops. By nesting two loops—the place the internal loop delays for two clock cycles—we will create a delay of 75,000,000 clock cycles.
One variable down, however how can we eradicate two extra? Happily, we don’t should. Whereas PIO solely supplies two general-purpose registers, x
and y
, it additionally consists of two special-purpose registers: osr
(output shift register) and isr
(enter shift register).
The PIO code that we’ll see in a second implements the backup beeper. Right here’s the way it works:
Initialization:
- The
pull(block)
instruction reads the half interval of the tone (75,000 clock cycles) intoosr
. - The worth is then copied to
isr
for later use. - The second
pull(block)
reads the interval rely (500 repeats), leaving the worth inosr
.
Beep Loops:
- The
mov(x, osr)
instruction copies the interval rely into thex
register, which serves because the outer loop counter. - For the internal loops,
mov(y, isr)
repeatedly copies the half interval intoy
to create delays for the excessive and low states of the tone.
Silence Loops:
- The silence loops mirror the construction of the beep loops however don’t set any pins, so that they act solely as a delay.
Wrap and Steady Execution:
- The
wrap_target()
andwrap()
directions outline the primary loop of the state machine. - After ending each the beep and silence loops, the state machine jumps again close to the beginning of this system, repeating the sequence indefinitely.
With this define in thoughts, right here’s the PIO meeting code for producing the backup beeper sign.
import rp2@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def back_up():
pull(block) # Learn the half interval of the beep sound.
mov(isr, osr) # Retailer the half interval in ISR.
pull(block) # Learn the period_count.
wrap_target() # Begin of the primary loop.
# Generate the beep sound.
mov(x, osr) # Load period_count into X.
label("beep_loop")
set(pins, 1) # Set the buzzer to excessive voltage (begin the tone).
mov(y, isr) # Load the half interval into Y.
label("beep_high_delay")
jmp(y_dec, "beep_high_delay") # Delay for the half interval.
set(pins, 0) # Set the buzzer to low voltage (finish the tone).
mov(y, isr) # Load the half interval into Y.
label("beep_low_delay")
jmp(y_dec, "beep_low_delay") # Delay for the low period.
jmp(x_dec, "beep_loop") # Repeat the beep loop.
# Silence between beeps.
mov(x, osr) # Load the interval rely into X for outer loop.
label("silence_loop")
mov(y, isr) # Load the half interval into Y for internal loop.
label("silence_delay")
jmp(y_dec, "silence_delay")[1] # Delay for 2 clock cycles (jmp + 1 further)
jmp(x_dec, "silence_loop") # Repeat the silence loop.
wrap() # Finish of the primary loop, jumps again to wrap_target for steady execution.
And right here’s the MicroPython code to configure and run the PIO program for the backup beeper. This script initializes the state machine, calculates timing values (half_period
and period_count
), and sends them to the PIO. It performs the beeping sequence for five seconds and stops. If linked to a desktop machine through USB, you possibly can cease it early with Ctrl-C
.
import machine
from machine import Pin
import timeBUZZER_PIN = 15
def demo_back_up():
print("Whats up, back_up!")
pio0 = rp2.PIO(0)
pio0.remove_program()
state_machine_frequency = machine.freq()
back_up_state_machine = rp2.StateMachine(0, back_up, set_base=Pin(BUZZER_PIN))
attempt:
back_up_state_machine.energetic(1)
half_period = int(state_machine_frequency / 1000 / 2)
period_count = int(state_machine_frequency * 0.5 / (half_period * 2))
print(f"half_period: {half_period}, period_count: {period_count}")
back_up_state_machine.put(half_period)
back_up_state_machine.put(period_count)
time.sleep_ms(5_000)
besides KeyboardInterrupt:
print("back_up demo stopped.")
lastly:
back_up_state_machine.energetic(0)
demo_back_up()
Right here’s what occurs if you run this system:
Apart: Operating this your self
The preferred Built-in Growth Surroundings (IDE) for programming the Raspberry Pi Pico with MicroPython is Thonny. Personally, I take advantage of the PyMakr extension for VS Code, although the MicroPico extension is one other fashionable alternative.
To listen to sound, I linked a passive buzzer, a resistor, and a transistor to the Pico. For detailed wiring diagrams and a elements checklist, take a look at the passive buzzer directions within the SunFounder’s Kepler Package.
Different Endings to the Register Starvation Video games
We used 4 registers — two normal and two particular — to resolve the problem. If this resolution feels lower than satisfying, listed below are various approaches to think about:
Use Constants: Why make half_period
, period_count
, and silence_cycles
variables in any respect? Hardcoding the constants “75,000,” “500,” and “75,000,000” may simplify the design. Nonetheless, PIO constants have limitations, which we’ll discover in Wat 5.
Pack Bits: Registers maintain 32 bits. Do we actually want two registers (2×32=64 bits) to retailer half_period
and period_count
? No. Storing 75,000 solely requires 17 bits, and 500 requires 9 bits. We may pack these right into a single register and use the out
instruction to shift values into x
and y
. This strategy would unlock both osr
or isr
for different duties, however solely one after the other—the opposite register should maintain the packed worth.
Gradual Movement: In MicroPython, you possibly can configure a PIO state machine to run at a slower frequency by merely specifying your required clock pace. MicroPython takes care of the main points for you, permitting the state machine to run as sluggish as ~2290 Hz. Operating the state machine at a slower pace implies that values like half_period
may be smaller, doubtlessly as small as 2
. Small values are simpler to hardcode as constants and extra compactly bit-packed into registers.
A Pleased Ending to the Register Starvation Video games
The Register Starvation Video games demanded strategic sacrifices and inventive workarounds, however we emerged victorious by leveraging PIO’s particular registers and intelligent looping buildings. If the stakes had been larger, various methods may have helped us adapt and survive.
However victory in a single enviornment doesn’t imply the challenges are over. Within the subsequent Wat, we face a brand new trial: PIO’s strict 32-instruction restrict.
Congratulations! You’ve bought a visit around the globe for simply $4. The catch? All of your belongings should match right into a tiny carry-on suitcase. Likewise, PIO packages permit you to create unbelievable performance, however each PIO program is restricted to only 32 directions.
Wat! Solely 32 directions? That’s not a lot area to pack all the things you want! However with intelligent planning, you possibly can normally make it work.
The Guidelines
- No PIO program may be longer than 32 directions.
- The
wrap_target
,label
, andwrap
directions don’t rely. - A Pico 1 consists of eight state machines, organized into two blocks of 4. A Pico 2 consists of twelve state machines, organized into three blocks of 4. Every block shares 32 instruction slots. So, as a result of all 4 state machines in a block draw from the identical 32-instruction pool, if one machine’s program makes use of all 32 slots, there’s no area left for the opposite three.
Avoiding Turbulence
For a clean flight, use MicroPython code to wash out any earlier packages from the block earlier than loading new ones. Right here’s tips on how to do it:
pio0 = rp2.PIO(0) # Block 0, containing state machines 0,1,2,3
pio0.remove_program() # Take away all packages from block 0
back_up_state_machine = rp2.StateMachine(0, back_up, set_base=Pin(BUZZER_PIN))
This ensures your instruction reminiscence is contemporary and prepared for takeoff. Clearing the blocks is particularly vital when utilizing the PyMakr extension’s “improvement mode.”
When Your Suitcase Gained’t Shut
In case your concept doesn’t match within the PIO instruction slots, these packing methods could assist. (Disclaimer: I haven’t tried all of those myself.)
- Swap PIO Applications on the Fly:
As an alternative of attempting to cram all the things into one program, contemplate swapping out packages mid-flight. Load solely what you want, if you want it. - Share Applications Throughout State Machines:
A number of state machines can run the identical program on the identical time. Every state machine could make the shared program behave otherwise based mostly on an enter worth. - Use MicroPython’s
exec
Command:
Save area by offloading directions to MicroPython. For instance, you possibly can execute initialization steps immediately from a string:
back_up_state_machine.exec("pull(block)")
- Use PIO’s exec instructions:
Inside your state machine, you possibly can execute instruction values saved inosr
without(exec)
or usemov(exec, x)
ormov(exec, y)
for registers. - Offload to the Predominant Processors:
If all else fails, transfer extra of your program to the Pico’s bigger twin processors — consider this as transport your further baggage to your vacation spot individually. The Pico SDK (part 3.1.4) calls this “bit banging”.
Along with your baggage now packed, let’s journey to the scene of a thriller.
In Wat 1, we programmed our audio {hardware} as a backup beeper. However that’s not what we’d like for our musical instrument. As an alternative, we would like a PIO program that performs a given tone indefinitely — till it’s informed to play a brand new one. This system also needs to wait silently when given a particular “relaxation” tone.
Resting till a brand new tone is offered is straightforward to program with pull(block)
—we’ll discover the main points under. Taking part in a tone at a selected frequency can be simple, constructing on the work we did in Wat 1.
However how can we verify for a brand new tone whereas persevering with to play the present one? The reply lies in utilizing “noblock” as an alternative of “block” in pull(noblock)
. Now, if there’s a brand new worth, will probably be loaded into osr
, permitting this system to replace seamlessly.
Right here’s the place the thriller begins: what occurs to osr
if pull(noblock)
known as and there’s no new worth?
I assumed it might preserve its earlier worth. Improper! Possibly it will get reset to 0? Improper once more! The stunning fact: it will get the worth of x
. Why? (No, not y
— x
.) As a result of the Pico SDK says so. Particularly, part 3.4.9.2 explains:
A nonblocking PULL on an empty FIFO has the identical impact as MOV OSR, X.
Realizing how pull(noblock)
works is vital, however there’s a much bigger lesson right here. Deal with the Pico SDK documentation just like the again of a thriller novel. Don’t attempt to clear up all the things by yourself—cheat! Skip to the “who carried out it” part, and in part 3.4, learn the superb particulars for every command you utilize. Studying just some paragraphs can prevent hours of confusion.
With this in thoughts, let’s have a look at a sensible instance. Beneath is the PIO program for taking part in tones and rests repeatedly. It makes use of pull(block)
to attend for enter throughout a relaxation and pull(noblock)
to verify for updates whereas taking part in a tone.
import rp2@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def sound():
# Relaxation till a brand new tone is obtained.
label("resting")
pull(block) # Look ahead to a brand new delay worth, preserve it in osr.
mov(x, osr) # Copy the delay into X.
jmp(not_x, "resting") # If new delay is zero, preserve resting.
# Play the tone till a brand new delay is obtained.
wrap_target() # Begin of the primary loop.
set(pins, 1) # Set the buzzer to excessive voltage.
label("high_voltage_loop")
jmp(x_dec, "high_voltage_loop") # Delay
set(pins, 0) # Set the buzzer to low voltage.
mov(x, osr) # Load the half interval into X.
label("low_voltage_loop")
jmp(x_dec, "low_voltage_loop") # Delay
# Learn any new delay worth. If none, preserve the present delay.
mov(x, osr) # set x, the default worth for "pull(noblock)"
pull(noblock) # Learn a brand new delay worth or use the default.
# If the brand new delay is zero, relaxation. In any other case, proceed taking part in the tone.
mov(x, osr) # Copy the delay into X.
jmp(not_x, "resting") # If X is zero, relaxation.
wrap() # Proceed taking part in the sound.
We’ll ultimately use this PIO program in our theremin-like musical instrument. For now, let’s see the PIO program in motion by taking part in a well-recognized melody. This demo makes use of “Twinkle, Twinkle, Little Star” to point out how one can management a melody by feeding frequencies and durations to the state machine. With just some traces of code, you can also make the Pico sing!
import rp2
import machine
from machine import Pin
import timefrom sound_pio import sound
BUZZER_PIN = 15
twinkle_twinkle = [
# Bar 1
(262, 400, "Twin-"), # C
(262, 400, "-kle"), # C
(392, 400, "twin-"), # G
(392, 400, "-kle"), # G
(440, 400, "lit-"), # A
(440, 400, "-tle"), # A
(392, 800, "star"), # G
(0, 400, ""), # rest
# Bar 2
(349, 400, "How"), # F
(349, 400, "I"), # F
(330, 400, "won-"), # E
(330, 400, "-der"), # E
(294, 400, "what"), # D
(294, 400, "you"), # D
(262, 800, "are"), # C
(0, 400, ""), # rest
]
def demo_sound():
print("Whats up, sound!")
pio0 = rp2.PIO(0)
pio0.remove_program()
state_machine_frequency = machine.freq()
sound_state_machine = rp2.StateMachine(0, sound, set_base=Pin(BUZZER_PIN))
attempt:
sound_state_machine.energetic(1)
for frequency, ms, lyrics in twinkle_twinkle:
if frequency > 0:
half_period = int(state_machine_frequency / frequency / 2)
print(f"'{lyrics}' -- Frequency: {frequency}")
# Ship the half interval to the PIO state machine
sound_state_machine.put(half_period)
time.sleep_ms(ms) # Wait because the tone performs
sound_state_machine.put(0) # Cease the tone
time.sleep_ms(50) # Give a brief pause between notes
else:
sound_state_machine.put(0) # Play a silent relaxation
time.sleep_ms(ms + 50) # Look ahead to the remaining period + a brief pause
besides KeyboardInterrupt:
print("Sound demo stopped.")
lastly:
sound_state_machine.energetic(0)
demo_sound()
Right here’s what occurs if you run this system:
We’ve solved one thriller, however there’s all the time one other problem lurking across the nook. In Wat 4, we’ll discover what occurs when your good {hardware} comes with a catch — it’s additionally very low-cost.
With sound working, we flip subsequent to measuring the gap to the musician’s hand utilizing the HC-SR04+ ultrasonic vary finder. This small however highly effective machine is on the market for lower than two {dollars}.
This little peripheral took me on an emotional curler coaster of “Wats!?”:
- Up: Amazingly, this $2 vary finder consists of its personal microcontroller, making it smarter and simpler to make use of.
- Down: Frustratingly, that very same “good” conduct is unintuitive.
- Up: Conveniently, the Pico can provide peripherals with both 3.3V or 5V energy.
- Down: Unpredictably, many vary finders are unreliable — or fail outright — at 3.3V, and so they can harm your Pico at 5V.
- Up: Fortunately, each broken vary finders and Picos are cheap to switch, and a dual-voltage model of the vary finder solved my issues.
Particulars
I initially assumed the vary finder would set the Echo pin excessive when the echo returned. I used to be improper.
As an alternative, the vary finder emits a sample of 8 ultrasonic pulses at 40 kHz (consider it as a backup beeper for canine). Instantly after, it units Echo excessive. The Pico ought to then begin measuring the time till Echo goes low, which alerts that the sensor detected the sample — or that it timed out.
As for voltage, the documentation specifies the vary finder operates at 5V. It appeared to work at 3.3V — till it didn’t. Across the identical time, my Pico stopped connecting to MicroPython IDEs, which depend on a particular USB protocol.
So, at this level each the Pico and the vary finder have been broken.
After experimenting with numerous cables, USB drivers, programming languages, and even an older 5V-only vary finder, I lastly resolved the difficulty with:
- A brand new Pico microcontroller that I already had available. (It was a Pico 2, however I don’t assume the mannequin issues.)
- A brand new dual-voltage 3.3/5V vary finder, nonetheless simply $2 per piece.
Wat 4: Classes Discovered
Because the curler coaster return to the station, I realized two key classes. First, due to microcontrollers, even easy {hardware} can behave in non-intuitive ways in which require cautious studying of the documentation. Second, whereas this {hardware} is intelligent, it’s additionally cheap — and meaning it’s liable to failure. When it fails, take a deep breath, bear in mind it’s just a few {dollars}, and substitute it.
{Hardware} quirks, nonetheless, are solely a part of the story. In Wat 5, in Half 2, we’ll shift our focus again to software program: the PIO programming language itself. We’ll uncover a conduct so sudden, it’d depart you questioning all the things you thought you knew about constants.